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 pathlib import Path
from datetime import datetime from datetime import datetime
import re 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 # Add scripts directory to path to allow importing from it
scripts_dir = Path(__file__).parent / "scripts" 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 = 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("/") @app.route("/")
def index(): def index():
# Redirect root to /fees for convenience while there are no other apps # 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" payments_url = f"https://docs.google.com/spreadsheets/d/{PAYMENTS_SHEET_ID}/edit"
members, sorted_months = get_members_with_fees() members, sorted_months = get_members_with_fees()
record_step("fetch_members")
if not members: if not members:
return "No data." return "No data."
@@ -40,6 +71,7 @@ def fees():
# Get exceptions for formatting # Get exceptions for formatting
credentials_path = ".secret/fuj-management-bot-credentials.json" credentials_path = ".secret/fuj-management-bot-credentials.json"
exceptions = fetch_exceptions(PAYMENTS_SHEET_ID, credentials_path) exceptions = fetch_exceptions(PAYMENTS_SHEET_ID, credentials_path)
record_step("fetch_exceptions")
formatted_results = [] formatted_results = []
for name, month_fees in results: for name, month_fees in results:
@@ -63,6 +95,8 @@ def fees():
row["months"].append({"cell": cell, "overridden": is_overridden}) row["months"].append({"cell": cell, "overridden": is_overridden})
formatted_results.append(row) formatted_results.append(row)
record_step("process_data")
return render_template( return render_template(
"fees.html", "fees.html",
months=[month_labels[m] for m in sorted_months], months=[month_labels[m] for m in sorted_months],
@@ -81,12 +115,16 @@ def reconcile_view():
credentials_path = ".secret/fuj-management-bot-credentials.json" credentials_path = ".secret/fuj-management-bot-credentials.json"
members, sorted_months = get_members_with_fees() members, sorted_months = get_members_with_fees()
record_step("fetch_members")
if not members: if not members:
return "No data." return "No data."
transactions = fetch_sheet_data(PAYMENTS_SHEET_ID, credentials_path) transactions = fetch_sheet_data(PAYMENTS_SHEET_ID, credentials_path)
record_step("fetch_payments")
exceptions = fetch_exceptions(PAYMENTS_SHEET_ID, credentials_path) exceptions = fetch_exceptions(PAYMENTS_SHEET_ID, credentials_path)
record_step("fetch_exceptions")
result = reconcile(members, sorted_months, transactions, exceptions) result = reconcile(members, sorted_months, transactions, exceptions)
record_step("reconcile")
# Format month labels # Format month labels
month_labels = { month_labels = {
@@ -128,6 +166,8 @@ def reconcile_view():
unmatched = result["unmatched"] unmatched = result["unmatched"]
import json import json
record_step("process_data")
return render_template( return render_template(
"reconcile.html", "reconcile.html",
months=[month_labels[m] for m in sorted_months], months=[month_labels[m] for m in sorted_months],
@@ -148,6 +188,7 @@ def payments():
credentials_path = ".secret/fuj-management-bot-credentials.json" credentials_path = ".secret/fuj-management-bot-credentials.json"
transactions = fetch_sheet_data(PAYMENTS_SHEET_ID, credentials_path) transactions = fetch_sheet_data(PAYMENTS_SHEET_ID, credentials_path)
record_step("fetch_payments")
# Group transactions by person # Group transactions by person
grouped = {} grouped = {}
@@ -171,6 +212,7 @@ def payments():
# Sort by date descending # Sort by date descending
grouped[p].sort(key=lambda x: str(x.get("date", "")), reverse=True) grouped[p].sort(key=lambda x: str(x.get("date", "")), reverse=True)
record_step("process_data")
return render_template( return render_template(
"payments.html", "payments.html",
grouped_payments=grouped, grouped_payments=grouped,

View File

@@ -148,6 +148,23 @@
.description a:hover { .description a:hover {
text-decoration: underline; 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> </style>
</head> </head>
@@ -199,6 +216,14 @@
</tfoot> </tfoot>
</table> </table>
</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>
</body> </body>
</html> </html>

View File

@@ -137,6 +137,23 @@
tr:hover { tr:hover {
background-color: #1a1a1a; 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> </style>
</head> </head>
@@ -183,6 +200,14 @@
{% endfor %} {% endfor %}
</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>
</body> </body>
</html> </html>

View File

@@ -343,6 +343,23 @@
color: #888; color: #888;
font-style: italic; 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> </style>
</head> </head>
@@ -485,6 +502,15 @@
</div> </div>
</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> <script>
const memberData = {{ member_data| safe }}; const memberData = {{ member_data| safe }};
const sortedMonths = {{ raw_months| tojson }}; const sortedMonths = {{ raw_months| tojson }};