bulk-questionnaire-upload/scripts/measure_perf.py
vee1e efe14f67a2
fix: preserve previous perf metrics when MongoDB unavailable; fix delete ID parsing
- update_readme_metrics.py: fall back to existing README values instead of
  overwriting with "N/A (MongoDB not connected)" when metrics are unavailable
- measure_perf.py: fix uploaded_id extraction — ParsedForm.id is top-level,
  not nested under a "form" key, so delete timing now actually runs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 15:09:46 +05:30

165 lines
6.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Measure API performance against the live Render deployment.
Outputs perf_results.json in the repo root.
"""
import json
import os
import sys
import time
import httpx
import pandas as pd
BASE_URL = os.getenv("BACKEND_URL", "https://bulk-questionnaire-upload.onrender.com")
TEST_FILE = os.path.join(os.path.dirname(__file__), "../tests/test_xlsforms_valid/valid_form_1.xlsx")
RUNS = 3
def timed_runs(fn, runs=RUNS):
times = []
last_result = None
for _ in range(runs):
t0 = time.perf_counter()
last_result = fn()
times.append((time.perf_counter() - t0) * 1000)
return {
"min_ms": round(min(times), 1),
"max_ms": round(max(times), 1),
"avg_ms": round(sum(times) / len(times), 1),
}, last_result
def log(msg):
print(msg, flush=True)
def main():
results = {}
# ── Warmup (wake Render free tier if sleeping) ──────────────────────────
log("Warming up server (may take up to 60s on free tier)...")
for attempt in range(3):
try:
r = httpx.get(f"{BASE_URL}/api/forms", timeout=90)
log(f" Warmup response: {r.status_code}")
break
except Exception as e:
log(f" Warmup attempt {attempt + 1} failed: {e}")
if attempt == 2:
log("Server unreachable aborting perf run.")
sys.exit(1)
time.sleep(10)
# ── Cold-start latency (first real request after warmup) ─────────────────
t0 = time.perf_counter()
httpx.get(f"{BASE_URL}/api/forms", timeout=30)
results["cold_start_ms"] = round((time.perf_counter() - t0) * 1000, 1)
log(f"Cold-start: {results['cold_start_ms']}ms")
# ── File Validation ───────────────────────────────────────────────────────
def do_validate():
with open(TEST_FILE, "rb") as f:
return httpx.post(
f"{BASE_URL}/api/validate",
files={"file": ("valid_form_1.xlsx", f,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
timeout=30,
)
try:
m, _ = timed_runs(do_validate)
results["validation"] = m
log(f"Validation: {m}")
except Exception as e:
log(f"Validation failed: {e}")
results["validation"] = None
# ── Form Parsing ──────────────────────────────────────────────────────────
def do_parse():
with open(TEST_FILE, "rb") as f:
return httpx.post(
f"{BASE_URL}/api/forms/parse",
files={"file": ("valid_form_1.xlsx", f,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")},
timeout=60,
)
try:
m, _ = timed_runs(do_parse)
results["parsing"] = m
log(f"Parsing: {m}")
except Exception as e:
log(f"Parsing failed: {e}")
results["parsing"] = None
# ── Form Upload (requires MongoDB) ────────────────────────────────────────
uploaded_id = None
def do_upload():
with open(TEST_FILE, "rb") as f:
return httpx.post(
f"{BASE_URL}/api/upload",
files=[("files", ("valid_form_1.xlsx", f,
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))],
timeout=90,
)
try:
m, resp = timed_runs(do_upload, runs=1)
if resp.status_code == 200:
results["upload"] = m
log(f"Upload: {m}")
data = resp.json()
for item in (data if isinstance(data, list) else [data]):
if isinstance(item, dict):
uploaded_id = item.get("id") or item.get("_id")
if uploaded_id:
break
else:
log(f"Upload returned {resp.status_code} skipping (MongoDB not connected?)")
results["upload"] = None
except Exception as e:
log(f"Upload skipped: {e}")
results["upload"] = None
# ── Per-question / per-option (derived from upload) ───────────────────────
df = pd.read_excel(TEST_FILE, sheet_name=None)
question_count = len(df.get("Questions Info", pd.DataFrame()))
option_count = len(df.get("Answer Options", pd.DataFrame()))
if results.get("upload") and question_count:
avg = results["upload"]["avg_ms"]
results["per_question_ms"] = round(avg / question_count, 3)
results["per_option_ms"] = round(avg / option_count, 3) if option_count else None
else:
results["per_question_ms"] = None
results["per_option_ms"] = None
# ── Delete (requires MongoDB) ─────────────────────────────────────────────
if uploaded_id:
def do_delete():
return httpx.delete(f"{BASE_URL}/api/forms/{uploaded_id}", timeout=30)
try:
m, _ = timed_runs(do_delete, runs=1)
results["delete"] = m
log(f"Delete: {m}")
except Exception as e:
log(f"Delete failed: {e}")
results["delete"] = None
else:
log("Delete skipped (no uploaded form to delete)")
results["delete"] = None
# ── Save results ──────────────────────────────────────────────────────────
out_path = os.path.join(os.path.dirname(__file__), "../perf_results.json")
with open(out_path, "w") as f:
json.dump(results, f, indent=2)
log(f"\nResults saved to perf_results.json")
log(json.dumps(results, indent=2))
if __name__ == "__main__":
main()