feat: add detailed performance profiling with interactive toggle
All checks were successful
Deploy to K8s / deploy (push) Successful in 11s
Build and Push / build (push) Successful in 9s

This commit is contained in:
Jan Novak
2026-03-02 22:34:06 +01:00
parent 7d05e3812c
commit b0276f68b3
4 changed files with 119 additions and 1 deletions

44
app.py
View File

@@ -2,7 +2,8 @@ import sys
from pathlib import Path
from datetime import datetime
import re
from flask import Flask, render_template
import time
from flask import Flask, render_template, g
# Add scripts directory to path to allow importing from it
scripts_dir = Path(__file__).parent / "scripts"
@@ -13,6 +14,35 @@ from match_payments import reconcile, fetch_sheet_data, fetch_exceptions, normal
app = Flask(__name__)
@app.before_request
def start_timer():
g.start_time = time.perf_counter()
g.steps = []
def record_step(name):
g.steps.append((name, time.perf_counter()))
@app.context_processor
def inject_render_time():
def get_render_time():
total = time.perf_counter() - g.start_time
breakdown = []
last_time = g.start_time
for name, timestamp in g.steps:
duration = timestamp - last_time
breakdown.append(f"{name}:{duration:.3f}s")
last_time = timestamp
# Add remaining time as 'render'
render_duration = time.perf_counter() - last_time
breakdown.append(f"render:{render_duration:.3f}s")
return {
"total": f"{total:.3f}",
"breakdown": " | ".join(breakdown)
}
return dict(get_render_time=get_render_time)
@app.route("/")
def index():
# Redirect root to /fees for convenience while there are no other apps
@@ -24,6 +54,7 @@ def fees():
payments_url = f"https://docs.google.com/spreadsheets/d/{PAYMENTS_SHEET_ID}/edit"
members, sorted_months = get_members_with_fees()
record_step("fetch_members")
if not members:
return "No data."
@@ -40,6 +71,7 @@ def fees():
# Get exceptions for formatting
credentials_path = ".secret/fuj-management-bot-credentials.json"
exceptions = fetch_exceptions(PAYMENTS_SHEET_ID, credentials_path)
record_step("fetch_exceptions")
formatted_results = []
for name, month_fees in results:
@@ -63,6 +95,8 @@ def fees():
row["months"].append({"cell": cell, "overridden": is_overridden})
formatted_results.append(row)
record_step("process_data")
return render_template(
"fees.html",
months=[month_labels[m] for m in sorted_months],
@@ -81,12 +115,16 @@ def reconcile_view():
credentials_path = ".secret/fuj-management-bot-credentials.json"
members, sorted_months = get_members_with_fees()
record_step("fetch_members")
if not members:
return "No data."
transactions = fetch_sheet_data(PAYMENTS_SHEET_ID, credentials_path)
record_step("fetch_payments")
exceptions = fetch_exceptions(PAYMENTS_SHEET_ID, credentials_path)
record_step("fetch_exceptions")
result = reconcile(members, sorted_months, transactions, exceptions)
record_step("reconcile")
# Format month labels
month_labels = {
@@ -128,6 +166,8 @@ def reconcile_view():
unmatched = result["unmatched"]
import json
record_step("process_data")
return render_template(
"reconcile.html",
months=[month_labels[m] for m in sorted_months],
@@ -148,6 +188,7 @@ def payments():
credentials_path = ".secret/fuj-management-bot-credentials.json"
transactions = fetch_sheet_data(PAYMENTS_SHEET_ID, credentials_path)
record_step("fetch_payments")
# Group transactions by person
grouped = {}
@@ -171,6 +212,7 @@ def payments():
# Sort by date descending
grouped[p].sort(key=lambda x: str(x.get("date", "")), reverse=True)
record_step("process_data")
return render_template(
"payments.html",
grouped_payments=grouped,

View File

@@ -148,6 +148,23 @@
.description a:hover {
text-decoration: underline;
}
.footer {
margin-top: 50px;
margin-bottom: 20px;
color: #333;
font-size: 9px;
text-align: center;
width: 100%;
cursor: pointer;
user-select: none;
}
.perf-breakdown {
display: none;
margin-top: 5px;
color: #222;
}
</style>
</head>
@@ -199,6 +216,14 @@
</tfoot>
</table>
</div>
{% set rt = get_render_time() %}
<div class="footer"
onclick="document.getElementById('perf-details').style.display = (document.getElementById('perf-details').style.display === 'block' ? 'none' : 'block')">
render time: {{ rt.total }}s
<div id="perf-details" class="perf-breakdown">
{{ rt.breakdown }}
</div>
</div>
</body>
</html>

View File

@@ -137,6 +137,23 @@
tr:hover {
background-color: #1a1a1a;
}
.footer {
margin-top: 50px;
margin-bottom: 20px;
color: #333;
font-size: 9px;
text-align: center;
width: 100%;
cursor: pointer;
user-select: none;
}
.perf-breakdown {
display: none;
margin-top: 5px;
color: #222;
}
</style>
</head>
@@ -183,6 +200,14 @@
{% endfor %}
</div>
{% set rt = get_render_time() %}
<div class="footer"
onclick="document.getElementById('perf-details').style.display = (document.getElementById('perf-details').style.display === 'block' ? 'none' : 'block')">
render time: {{ rt.total }}s
<div id="perf-details" class="perf-breakdown">
{{ rt.breakdown }}
</div>
</div>
</body>
</html>

View File

@@ -343,6 +343,23 @@
color: #888;
font-style: italic;
}
.footer {
margin-top: 50px;
margin-bottom: 20px;
color: #333;
font-size: 9px;
text-align: center;
width: 100%;
cursor: pointer;
user-select: none;
}
.perf-breakdown {
display: none;
margin-top: 5px;
color: #222;
}
</style>
</head>
@@ -485,6 +502,15 @@
</div>
</div>
{% set rt = get_render_time() %}
<div class="footer"
onclick="document.getElementById('perf-details').style.display = (document.getElementById('perf-details').style.display === 'block' ? 'none' : 'block')">
render time: {{ rt.total }}s
<div id="perf-details" class="perf-breakdown">
{{ rt.breakdown }}
</div>
</div>
<script>
const memberData = {{ member_data| safe }};
const sortedMonths = {{ raw_months| tojson }};