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
138 lines
4.6 KiB
Python
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()
|