Spaces:
Running
Running
| """ | |
| AgriFlow Matching Engine — Latency Benchmark | |
| ============================================= | |
| Measure end-to-end latency dengan multiple konfigurasi untuk memvalidasi | |
| klaim "<500ms p99 untuk 38 kab × 19 komoditas" (Section 5.5.4). | |
| Run: | |
| python benchmarks/latency.py | |
| Output: | |
| Tabel p50/p95/p99/max latency untuk: | |
| - Sample data current (40 supply × 33 deficit) | |
| - Stress: synthetic 38 kab × 19 komoditas full | |
| - Stress: 100 supply × 100 deficit (large) | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import statistics | |
| import sys | |
| import time | |
| from datetime import datetime | |
| # Force UTF-8 stdio di Windows. | |
| if sys.platform == "win32": | |
| try: | |
| sys.stdout.reconfigure(encoding="utf-8") | |
| except (AttributeError, OSError): | |
| pass | |
| # Add project root ke path | |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | |
| from matching_engine import ( | |
| Commodity, DemandNode, Kabupaten, LogisticsContext, SupplyNode, Tier, | |
| run_matching, | |
| ) | |
| from sample_data import load_all_sample_data | |
| from sample_data.generate_sample_data import KABUPATEN_DATA, KOMODITAS_DATA | |
| def percentile(data, p): | |
| s = sorted(data) | |
| k = (len(s) - 1) * p | |
| f = int(k) | |
| c = min(f + 1, len(s) - 1) | |
| return s[f] + (s[c] - s[f]) * (k - f) | |
| def bench_one(label: str, supply, demand, weather=None, historical=None, | |
| warmup: int = 5, iterations: int = 100) -> dict: | |
| logistics = LogisticsContext() | |
| # Warmup | |
| for _ in range(warmup): | |
| run_matching(supply, demand, logistics=logistics, | |
| weather_forecasts=weather, historical_prices=historical) | |
| # Measure | |
| samples = [] | |
| for _ in range(iterations): | |
| t = time.perf_counter() | |
| run_matching(supply, demand, logistics=logistics, | |
| weather_forecasts=weather, historical_prices=historical) | |
| samples.append((time.perf_counter() - t) * 1000) | |
| return { | |
| "label": label, | |
| "n_supply": len(supply), | |
| "n_demand": len(demand), | |
| "iterations": iterations, | |
| "p50_ms": percentile(samples, 0.50), | |
| "p95_ms": percentile(samples, 0.95), | |
| "p99_ms": percentile(samples, 0.99), | |
| "min_ms": min(samples), | |
| "max_ms": max(samples), | |
| "mean_ms": statistics.mean(samples), | |
| } | |
| def make_synthetic_full_jatim(): | |
| """38 kab × 19 komoditas: setengah surplus, setengah deficit per komoditas.""" | |
| kabs = [] | |
| for kid, nama, lat, lon, ipm, pop, tier_s in KABUPATEN_DATA: | |
| kabs.append(Kabupaten( | |
| id=kid, nama=nama, latitude=lat, longitude=lon, ipm=ipm, | |
| tier=Tier.HIGH if tier_s == "TIER_1_HIGH" else Tier.MEDIUM, | |
| population=pop, | |
| )) | |
| komos = [ | |
| Commodity(code=c, nama=n, max_distance_km=md, min_viable_tons=mv, | |
| max_fresh_age_days=mf) | |
| for c, n, md, mv, mf, _baseline in KOMODITAS_DATA | |
| ] | |
| surplus, deficit = [], [] | |
| for komo in komos: | |
| for i, kab in enumerate(kabs): | |
| if i % 2 == 0: | |
| surplus.append(SupplyNode( | |
| kabupaten=kab, commodity=komo, | |
| volume_tons=max(komo.min_viable_tons * 5, 20.0), | |
| price_per_kg=30000, harvest_age_days=1, | |
| )) | |
| else: | |
| deficit.append(DemandNode( | |
| kabupaten=kab, commodity=komo, | |
| volume_tons=max(komo.min_viable_tons * 4, 15.0), | |
| price_per_kg=50000, | |
| )) | |
| return surplus, deficit | |
| def make_synthetic_large(n_supply: int = 100, n_demand: int = 100): | |
| """Stress test: scale beyond Jatim (project to national scale).""" | |
| s, d = make_synthetic_full_jatim() | |
| # Pad dengan duplikat (anggap nasional projection) | |
| while len(s) < n_supply: | |
| s.extend(s[:n_supply - len(s)]) | |
| while len(d) < n_demand: | |
| d.extend(d[:n_demand - len(d)]) | |
| return s[:n_supply], d[:n_demand] | |
| def print_row(r): | |
| print(f" {r['label']:<35s} " | |
| f"{r['n_supply']:>4d}×{r['n_demand']:<4d} " | |
| f"p50={r['p50_ms']:>6.2f} p95={r['p95_ms']:>6.2f} " | |
| f"p99={r['p99_ms']:>7.2f} max={r['max_ms']:>7.2f} " | |
| f"mean={r['mean_ms']:>6.2f} ms") | |
| def main(): | |
| print("=" * 110) | |
| print(f" AgriFlow Matching Engine — Latency Benchmark " | |
| f"({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})") | |
| print("=" * 110) | |
| print(f" Target Section 5.5.4: <500ms p99 untuk 38 kab × 19 komoditas") | |
| print() | |
| results = [] | |
| # 1. Sample data current | |
| data = load_all_sample_data() | |
| results.append(bench_one( | |
| "Sample data CSV (realistic)", | |
| data["surplus"], data["deficit"], | |
| weather=data["weather"], historical=data["historical_prices"], | |
| )) | |
| # 2. Synthetic 38 kab × 19 komoditas full | |
| s_full, d_full = make_synthetic_full_jatim() | |
| results.append(bench_one( | |
| "Synthetic full Jatim (38×19)", | |
| s_full, d_full, | |
| )) | |
| # 3. Stress 100x100 | |
| s_100, d_100 = make_synthetic_large(100, 100) | |
| results.append(bench_one( | |
| "Stress 100×100 (national scale)", | |
| s_100, d_100, | |
| iterations=50, | |
| )) | |
| # 4. Stress 200x200 | |
| s_200, d_200 = make_synthetic_large(200, 200) | |
| results.append(bench_one( | |
| "Stress 200×200", | |
| s_200, d_200, | |
| iterations=30, | |
| )) | |
| print(f" {'Configuration':<35s} {'N (s×d)':>9s} {'p50':>9s} " | |
| f"{'p95':>9s} {'p99':>11s} {'max':>11s} {'mean':>11s}") | |
| print(" " + "-" * 106) | |
| for r in results: | |
| print_row(r) | |
| print() | |
| target = 500.0 | |
| p99_max = max(r["p99_ms"] for r in results) | |
| if p99_max < target: | |
| print(f" PASS semua konfigurasi p99 < {target}ms target " | |
| f"(highest p99 = {p99_max:.2f}ms, " | |
| f"margin {(target - p99_max) / target * 100:.1f}%)") | |
| else: | |
| print(f" FAIL p99 {p99_max:.2f}ms melebihi target {target}ms") | |
| print() | |
| if __name__ == "__main__": | |
| main() | |