Spaces:
Running on Zero
Running on Zero
feat: resolve nearby masjids 500 error, add popular sanctuaries, and optimize search architecture
ae9655d Download scripts/benchmark_multilingual_concurrency.py from sahihislam/superadmin: direct link, hf CLI and curl.
- Browser
- Download file 14.5 kB
-
https://huggingface.co/spaces/sahihislam/superadmin/resolve/main/scripts/benchmark_multilingual_concurrency.py
- Command line
-
hf download hf://spaces/sahihislam/superadmin/scripts/benchmark_multilingual_concurrency.py
-
curl -L -o benchmark_multilingual_concurrency.py https://huggingface.co/spaces/sahihislam/superadmin/resolve/main/scripts/benchmark_multilingual_concurrency.py
14.5 kB
| """ | |
| Sahih Islam β Multilingual Concurrency & Telemetry Load Test | |
| Tests 50, 100, and 200 concurrent users with realistic mixed-language requests: | |
| - Languages: English (40%), Urdu (35%), Hinglish (20%), Arabic (5%) | |
| - Tiers: 50 users, 100 users, 200 users | |
| - Telemetry: | |
| β’ Cache hit/miss p50, p95, p99 latency | |
| β’ Canonical cache hit rate & full response cache hit rate | |
| β’ Per-request Atlas queries & LLM calls | |
| β’ Redis & L1 memory consumption / process RSS | |
| """ | |
| import sys | |
| import os | |
| import io | |
| import time | |
| import json | |
| import random | |
| import asyncio | |
| from typing import Dict, List, Any, Tuple | |
| if sys.platform == "win32": | |
| try: | |
| sys.stdout.reconfigure(encoding="utf-8", errors="replace") | |
| sys.stderr.reconfigure(encoding="utf-8", errors="replace") | |
| except Exception: | |
| sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") | |
| sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") | |
| import httpx | |
| import psutil | |
| # Core Islamic topic queries (simulating high-frequency questions) | |
| CORE_QUERIES = [ | |
| "virtues of prayer salah on time", | |
| "fasting rules in ramadan", | |
| "rights of neighbors in islam", | |
| "patience during trials and tribulations sabr", | |
| "manners of speaking and honesty in speech", | |
| "seeking knowledge is obligatory for every muslim", | |
| "supplication dua acceptance conditions", | |
| "charity sadaqah and removing harm from path", | |
| "night of decree laylatul qadr virtues", | |
| "kindness to parents mother and father", | |
| ] | |
| # Tail Islamic queries (simulating occasional long-tail searches) | |
| TAIL_QUERIES = [ | |
| "rules of inheritance distribution for daughters", | |
| "prostration of forgetfulness sajdah sahw in prayer", | |
| "etiquette of slaughtering animals dhabihah", | |
| "borrowing money with collateral rahn", | |
| "virtue of reciting surah al-kahf on friday", | |
| ] | |
| # Language distribution weights: English 40%, Urdu 35%, Hinglish 20%, Arabic 5% | |
| LANGUAGES_POOL = ["English"] * 40 + ["Urdu"] * 35 + ["Hinglish"] * 20 + ["Arabic"] * 5 | |
| def get_backend_pid() -> int: | |
| """Finds the Uvicorn backend process PID listening on 7860.""" | |
| for proc in psutil.process_iter(['pid', 'name', 'cmdline']): | |
| try: | |
| cmd = " ".join(proc.info['cmdline'] or []) | |
| if "run.py" in cmd or "uvicorn" in cmd: | |
| return proc.info['pid'] | |
| except (psutil.NoSuchProcess, psutil.AccessDenied): | |
| pass | |
| return os.getpid() | |
| async def get_redis_stats() -> Dict[str, Any]: | |
| """Retrieves live Redis memory and key count.""" | |
| try: | |
| from app.infrastructure.redis import redis_manager | |
| await redis_manager.init() | |
| if redis_manager._client: | |
| info = await redis_manager._client.info('memory') | |
| keys = await redis_manager._client.dbsize() | |
| return { | |
| "used_memory_human": info.get("used_memory_human", "N/A"), | |
| "peak_memory_human": info.get("used_memory_peak_human", "N/A"), | |
| "total_keys": keys, | |
| } | |
| else: | |
| return { | |
| "used_memory_human": "in-memory", | |
| "total_keys": len(redis_manager._memory_cache), | |
| } | |
| except Exception as e: | |
| return {"error": str(e), "total_keys": 0, "used_memory_human": "N/A"} | |
| def calc_percentiles(latencies: List[float]) -> Dict[str, float]: | |
| """Calculates p50, p90, p95, p99 min and max.""" | |
| if not latencies: | |
| return {"p50": 0.0, "p90": 0.0, "p95": 0.0, "p99": 0.0, "min": 0.0, "max": 0.0} | |
| s = sorted(latencies) | |
| n = len(s) | |
| return { | |
| "p50": round(s[int(0.50 * n)], 1), | |
| "p90": round(s[min(n - 1, int(0.90 * n))], 1), | |
| "p95": round(s[min(n - 1, int(0.95 * n))], 1), | |
| "p99": round(s[min(n - 1, int(0.99 * n))], 1), | |
| "min": round(s[0], 1), | |
| "max": round(s[-1], 1), | |
| } | |
| async def simulate_user( | |
| user_id: int, | |
| client: httpx.AsyncClient, | |
| base_url: str, | |
| requests_per_user: int, | |
| results: List[Dict[str, Any]], | |
| lock: asyncio.Lock | |
| ): | |
| """Simulates a concurrent user submitting mixed-language Hadith queries.""" | |
| for req_idx in range(requests_per_user): | |
| # 80% chance popular core query (higher cache reuse), 20% long-tail | |
| if random.random() < 0.80: | |
| query = random.choice(CORE_QUERIES) | |
| else: | |
| query = random.choice(TAIL_QUERIES) | |
| lang = random.choice(LANGUAGES_POOL) | |
| t0 = time.perf_counter() | |
| status_code = 0 | |
| is_success = False | |
| text_received = "" | |
| is_hit = False | |
| try: | |
| resp = await client.post( | |
| f"{base_url}/api/ask", | |
| json={ | |
| "query": query, | |
| "language": lang, | |
| "top_k": 3, | |
| "strict_mode": False, | |
| "history": [], | |
| }, | |
| timeout=35.0 | |
| ) | |
| lat_ms = (time.perf_counter() - t0) * 1000.0 | |
| status_code = resp.status_code | |
| if status_code == 200: | |
| is_success = True | |
| text_received = resp.text | |
| # If latency is under 200ms, it's served from cache (Tier 1 or Tier 1b) | |
| if lat_ms < 250.0: | |
| is_hit = True | |
| except Exception as e: | |
| lat_ms = (time.perf_counter() - t0) * 1000.0 | |
| status_code = 500 | |
| record = { | |
| "user_id": user_id, | |
| "req_idx": req_idx, | |
| "query": query, | |
| "language": lang, | |
| "latency_ms": lat_ms, | |
| "status_code": status_code, | |
| "success": is_success, | |
| "is_hit": is_hit, | |
| "content_len": len(text_received), | |
| } | |
| async with lock: | |
| results.append(record) | |
| # Realistic user think-time between queries (10ms - 40ms) | |
| await asyncio.sleep(random.uniform(0.01, 0.04)) | |
| async def run_concurrency_tier( | |
| num_users: int, | |
| requests_per_user: int, | |
| base_url: str, | |
| backend_pid: int | |
| ) -> Dict[str, Any]: | |
| """Executes a load test tier with num_users concurrent clients.""" | |
| print(f"\n================================================================================") | |
| print(f"π EXECUTING TIER: {num_users} CONCURRENT USERS ({num_users * requests_per_user} TOTAL REQUESTS)") | |
| print(f"================================================================================") | |
| # Pre-tier memory check | |
| try: | |
| proc = psutil.Process(backend_pid) | |
| pre_rss_mb = proc.memory_info().rss / 1024 / 1024 | |
| except Exception: | |
| pre_rss_mb = 0.0 | |
| pre_redis = await get_redis_stats() | |
| limits = httpx.Limits(max_connections=max(250, num_users + 50), max_keepalive_connections=max(200, num_users)) | |
| async with httpx.AsyncClient(limits=limits, timeout=40.0) as client: | |
| results: List[Dict[str, Any]] = [] | |
| lock = asyncio.Lock() | |
| t_start = time.perf_counter() | |
| tasks = [ | |
| simulate_user(uid, client, base_url, requests_per_user, results, lock) | |
| for uid in range(num_users) | |
| ] | |
| await asyncio.gather(*tasks) | |
| total_time_s = time.perf_counter() - t_start | |
| # Post-tier memory check | |
| try: | |
| post_rss_mb = proc.memory_info().rss / 1024 / 1024 | |
| except Exception: | |
| post_rss_mb = 0.0 | |
| post_redis = await get_redis_stats() | |
| # Process metrics | |
| total_reqs = len(results) | |
| success_reqs = sum(1 for r in results if r["success"]) | |
| fail_reqs = total_reqs - success_reqs | |
| rps = total_reqs / total_time_s if total_time_s > 0 else 0.0 | |
| hit_results = [r for r in results if r["is_hit"] and r["success"]] | |
| miss_results = [r for r in results if not r["is_hit"] and r["success"]] | |
| all_lats = [r["latency_ms"] for r in results] | |
| hit_lats = [r["latency_ms"] for r in hit_results] | |
| miss_lats = [r["latency_ms"] for r in miss_results] | |
| all_p = calc_percentiles(all_lats) | |
| hit_p = calc_percentiles(hit_lats) | |
| miss_p = calc_percentiles(miss_lats) | |
| # Estimate Canonical vs Full Response Hit Breakdown | |
| # Latencies < 25ms are Full Response RAM hits, 25ms - 250ms are Canonical Multilingual hits | |
| full_resp_hits = sum(1 for r in hit_results if r["latency_ms"] < 25.0) | |
| canonical_hits = sum(1 for r in hit_results if r["latency_ms"] >= 25.0) | |
| total_hits = len(hit_results) | |
| hit_rate_pct = (total_hits / max(1, total_reqs)) * 100.0 | |
| canonical_pct = (canonical_hits / max(1, total_reqs)) * 100.0 | |
| full_resp_pct = (full_resp_hits / max(1, total_reqs)) * 100.0 | |
| # Avoided calls: | |
| # Each canonical hit avoided 1 Atlas vector search + 3 translation calls | |
| # Each full response hit avoided 1 Atlas search + 1 query rewrite + 3 translation calls | |
| atlas_avoided = total_hits | |
| llm_translations_avoided = total_hits * 3 | |
| llm_rewrites_avoided = full_resp_hits | |
| # Language breakdown | |
| lang_counts = {} | |
| for r in results: | |
| lang_counts[r["language"]] = lang_counts.get(r["language"], 0) + 1 | |
| print(f"β±οΈ Duration: {total_time_s:.2f}s | Throughput: {rps:.2f} RPS | Success Rate: {(success_reqs/max(1,total_reqs))*100:.1f}%") | |
| print(f"π Latency Percentiles (Overall): p50={all_p['p50']}ms | p90={all_p['p90']}ms | p95={all_p['p95']}ms | p99={all_p['p99']}ms") | |
| print(f"β‘ Cache Hit Latencies (p50/p95): p50={hit_p['p50']}ms | p95={hit_p['p95']}ms | min={hit_p['min']}ms | max={hit_p['max']}ms") | |
| print(f"βοΈ Cache Miss Latencies (p50/p95): p50={miss_p['p50']}ms | p95={miss_p['p95']}ms | min={miss_p['min']}ms | max={miss_p['max']}ms") | |
| print(f"π― Total Hit Rate: {hit_rate_pct:.1f}% (Canonical Hit Rate: {canonical_pct:.1f}%, Full Response Hit Rate: {full_resp_pct:.1f}%)") | |
| print(f"π‘οΈ Calls Avoided: Atlas Queries={atlas_avoided} | LLM Translations={llm_translations_avoided} | LLM Rewrites={llm_rewrites_avoided}") | |
| print(f"πΎ Memory: Process RSS={post_rss_mb:.1f}MB (Ξ {post_rss_mb - pre_rss_mb:+.1f}MB) | Redis Keys={post_redis.get('total_keys', 0)} ({post_redis.get('used_memory_human', 'N/A')})") | |
| print(f"π Languages: {lang_counts}") | |
| return { | |
| "num_users": num_users, | |
| "total_requests": total_reqs, | |
| "success_requests": success_reqs, | |
| "failed_requests": fail_reqs, | |
| "duration_seconds": round(total_time_s, 2), | |
| "rps": round(rps, 2), | |
| "hit_rate_pct": round(hit_rate_pct, 1), | |
| "canonical_hit_rate_pct": round(canonical_pct, 1), | |
| "full_response_hit_rate_pct": round(full_resp_pct, 1), | |
| "atlas_avoided": atlas_avoided, | |
| "llm_translations_avoided": llm_translations_avoided, | |
| "llm_rewrites_avoided": llm_rewrites_avoided, | |
| "percentiles_overall": all_p, | |
| "percentiles_cache_hit": hit_p, | |
| "percentiles_cache_miss": miss_p, | |
| "process_rss_pre_mb": round(pre_rss_mb, 1), | |
| "process_rss_post_mb": round(post_rss_mb, 1), | |
| "redis_keys": post_redis.get("total_keys", 0), | |
| "redis_memory": post_redis.get("used_memory_human", "N/A"), | |
| "languages": lang_counts, | |
| } | |
| async def main(): | |
| base_url = "http://127.0.0.1:7860" | |
| backend_pid = get_backend_pid() | |
| print("================================================================================") | |
| print("π SAHIH ISLAM: SUSTAINED MULTILINGUAL CONCURRENCY LOAD TEST") | |
| print(" Testing 50, 100, and 200 Concurrent Users with Realistic Mixed-Language Traffic") | |
| print(" Tracking: Latency Percentiles (p50/p95), Cache Hit Rates, External Calls, Memory") | |
| print("================================================================================") | |
| print(f"Backend Server: {base_url} (Target PID: {backend_pid})\n") | |
| # Warm-up phase: Pre-seed core queries in English so canonical translations are available | |
| print("π₯ PRE-WARMING: Priming canonical translation records for core Islamic topics...") | |
| limits = httpx.Limits(max_connections=20, max_keepalive_connections=10) | |
| async with httpx.AsyncClient(limits=limits, timeout=30.0) as client: | |
| for q in CORE_QUERIES[:6]: | |
| try: | |
| await client.post(f"{base_url}/api/ask", json={ | |
| "query": q, | |
| "language": "English", | |
| "top_k": 3, | |
| "strict_mode": False, | |
| }) | |
| except Exception: | |
| pass | |
| await asyncio.sleep(0.8) | |
| print("β Warm-up complete. Canonical translations ready for mixed-language queries.\n") | |
| tier_results = [] | |
| # Tier 1: 50 Concurrent Users (3 requests each = 150 requests) | |
| t1 = await run_concurrency_tier(50, 3, base_url, backend_pid) | |
| tier_results.append(t1) | |
| await asyncio.sleep(1.0) | |
| # Tier 2: 100 Concurrent Users (3 requests each = 300 requests) | |
| t2 = await run_concurrency_tier(100, 3, base_url, backend_pid) | |
| tier_results.append(t2) | |
| await asyncio.sleep(1.0) | |
| # Tier 3: 200 Concurrent Users (3 requests each = 600 requests) | |
| t3 = await run_concurrency_tier(200, 3, base_url, backend_pid) | |
| tier_results.append(t3) | |
| # Save final results JSON | |
| out_path = os.path.join(os.path.dirname(__file__), "..", "multilingual_concurrency_test_results.json") | |
| with open(out_path, "w", encoding="utf-8") as f: | |
| json.dump(tier_results, f, indent=2) | |
| print("\n================================================================================") | |
| print("π FINAL CONCURRENCY BENCHMARK SUMMARY TABLE") | |
| print("================================================================================") | |
| print(f"{'Users':<6} | {'RPS':<7} | {'Success':<8} | {'Hit Rate':<9} | {'Canon Hit':<10} | {'Hit p50':<9} | {'Hit p95':<9} | {'Miss p50':<10} | {'RAM (MB)':<8}") | |
| print("-" * 92) | |
| for t in tier_results: | |
| u = t["num_users"] | |
| r = t["rps"] | |
| s = f"{(t['success_requests']/max(1, t['total_requests']))*100:.1f}%" | |
| h = f"{t['hit_rate_pct']:.1f}%" | |
| c = f"{t['canonical_hit_rate_pct']:.1f}%" | |
| hp50 = f"{t['percentiles_cache_hit']['p50']}ms" | |
| hp95 = f"{t['percentiles_cache_hit']['p95']}ms" | |
| mp50 = f"{t['percentiles_cache_miss']['p50']}ms" | |
| ram = f"{t['process_rss_post_mb']}MB" | |
| print(f"{u:<6} | {r:<7.1f} | {s:<8} | {h:<9} | {c:<10} | {hp50:<9} | {hp95:<9} | {mp50:<10} | {ram:<8}") | |
| print("-" * 92) | |
| print(f"Results persisted to: {out_path}") | |
| if __name__ == "__main__": | |
| asyncio.run(main()) | |