#!/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()