Files
Autoparts-DB/scripts/benchmark_async_catalog.py
consultoria-as a1be8dd0ea OPCIÓN A: A2 Virtual Scroll + A3 Celery + A4 asyncpg PoC + A5 particionamiento
A2 — Virtual scroll en tablas grandes:
- Nuevo helper VirtualScroll en pos/static/js/virtual-scroll.js
- inventory.js: tabla de productos con virtual scroll
- customers.js: tabla de clientes con virtual scroll
- fleet.js: renderMaintenance() y renderHistory() con virtual scroll
- Templates envueltos en .vs-container para scroll

A3 — Celery worker queue:
- pos/celery_app.py + pos/tasks.py (warm cache, bulk import, reports)
- Blueprint tasks_bp.py con endpoints /pos/api/tasks/*
- Script scripts/start_celery.sh

A4 — asyncpg + Quart PoC:
- pos/async_catalog.py: endpoint /pos/api/catalog/async-search
- scripts/benchmark_async_catalog.py: benchmark Flask vs Quart

A5 — Particionar vehicle_parts:
- scripts/partition_vehicle_parts.py: migración segura por hash (16 particiones)
- Soporta --dry-run, --skip-swap, --skip-drop

Tests: 36/36 pasando
2026-04-27 09:53:36 +00:00

138 lines
4.6 KiB
Python

#!/usr/bin/env python3
"""Benchmark: compare Flask sync vs Quart async catalog search.
Prerequisites:
- Flask POS server running on http://localhost:5001
- Quart async server running on http://localhost:5002
(start with: cd pos && hypercorn async_catalog:app --bind 0.0.0.0:5002)
Usage:
python3 benchmark_async_catalog.py --workers 20 --requests 200
"""
import argparse
import asyncio
import json
import statistics
import sys
import time
import urllib.request
from concurrent.futures import ThreadPoolExecutor, as_completed
def sync_request(url):
req = urllib.request.Request(url)
start = time.perf_counter()
try:
with urllib.request.urlopen(req, timeout=30) as resp:
_ = resp.read()
return (time.perf_counter() - start) * 1000, resp.status, None
except Exception as e:
return (time.perf_counter() - start) * 1000, 0, str(e)
async def async_request(session, url):
import aiohttp
start = time.perf_counter()
try:
async with session.get(url) as resp:
_ = await resp.read()
return (time.perf_counter() - start) * 1000, resp.status, None
except Exception as e:
return (time.perf_counter() - start) * 1000, 0, str(e)
def benchmark_sync(url, workers, requests_total):
latencies = []
errors = []
with ThreadPoolExecutor(max_workers=workers) as ex:
futures = [ex.submit(sync_request, url) for _ in range(requests_total)]
for f in as_completed(futures):
latency, status, err = f.result()
if err:
errors.append((status, err))
else:
latencies.append(latency)
return latencies, errors
async def benchmark_async(url, workers, requests_total):
import aiohttp
latencies = []
errors = []
connector = aiohttp.TCPConnector(limit=workers * 2)
async with aiohttp.ClientSession(connector=connector) as session:
sem = asyncio.Semaphore(workers)
async def task():
async with sem:
return await async_request(session, url)
results = await asyncio.gather(*[task() for _ in range(requests_total)])
for latency, status, err in results:
if err:
errors.append((status, err))
else:
latencies.append(latency)
return latencies, errors
def report(label, latencies, errors, duration):
if not latencies:
print(f"{label}: NO successful requests")
return
lat = sorted(latencies)
p50 = lat[int(len(lat) * 0.5)]
p95 = lat[int(len(lat) * 0.95)]
p99 = lat[int(len(lat) * 0.99)]
mean = statistics.mean(lat)
rps = len(lat) / duration if duration > 0 else 0
print(f"{label:20s} | mean={mean:7.1f}ms | p50={p50:7.1f}ms | p95={p95:7.1f}ms | p99={p99:7.1f}ms | OK={len(lat)} | Err={len(errors)} | RPS={rps:.1f}")
def main():
parser = argparse.ArgumentParser()
parser.add_argument('--flask-url', default='http://localhost:5001/pos/api/catalog/search?q=filtro%20aire&limit=20')
parser.add_argument('--quart-url', default='http://localhost:5002/pos/api/catalog/async-search?q=filtro%20aire&limit=20')
parser.add_argument('--workers', '-w', type=int, default=20)
parser.add_argument('--requests', '-n', type=int, default=200)
args = parser.parse_args()
print("=" * 100)
print(f"Benchmark: {args.requests} requests, {args.workers} concurrent workers")
print("=" * 100)
# Sync (Flask)
print("\n[1/2] Warming up Flask...")
sync_request(args.flask_url)
print("[1/2] Benchmarking Flask (sync)...")
start = time.time()
lat_sync, err_sync = benchmark_sync(args.flask_url, args.workers, args.requests)
dur_sync = time.time() - start
report("Flask sync", lat_sync, err_sync, dur_sync)
# Async (Quart)
print("\n[2/2] Warming up Quart...")
asyncio.run(benchmark_async(args.quart_url, 5, 1))
print("[2/2] Benchmarking Quart (async)...")
start = time.time()
lat_async, err_async = asyncio.run(benchmark_async(args.quart_url, args.workers, args.requests))
dur_async = time.time() - start
report("Quart async", lat_async, err_async, dur_async)
print("\n" + "=" * 100)
if lat_sync and lat_async:
improvement = (statistics.mean(lat_sync) - statistics.mean(lat_async)) / statistics.mean(lat_sync) * 100
print(f"Mean latency improvement: {improvement:+.1f}%")
print("=" * 100)
if __name__ == '__main__':
try:
import aiohttp
except ImportError:
print("ERROR: aiohttp is required for async benchmark.")
print("Install: pip install aiohttp --break-system-packages")
sys.exit(1)
main()