memoryweave/scripts/benchmark.py

154 lines
5.2 KiB
Python

#!/usr/bin/env python3
import json
import urllib.request
import time
import statistics
import concurrent.futures
API_KEY = "zhiyi-dev-key-2026"
def api(method, path, data=None):
req = urllib.request.Request(
f"http://localhost:7821{path}",
data=json.dumps(data).encode() if data else None,
headers={"X-API-Key": API_KEY, "Content-Type": "application/json"},
method=method
)
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
except Exception as e:
if "429" in str(e):
time.sleep(0.5)
return api(method, path, data) # retry once
raise
def time_request(method, path, data=None):
start = time.perf_counter()
api(method, path, data)
return (time.perf_counter() - start) * 1000 # ms
# Warmup
time.sleep(1)
for _ in range(3):
api("POST", "/api/v1/recall", {"query": "test", "limit": 5, "namespace": "shared"})
time.sleep(0.1)
# === 1. Recall ===
print("=== Semantic Recall Test ===")
recall_tests = [
("牧尘的系统是什么", "Arch"),
("牧尘的内存多大", "GB"),
("Docker", "容器"),
("织忆图谱", "图谱"),
("测试时间", "2026"),
("编译", "编译"),
("接口", "API"),
("memoryweave", "织忆"),
("MiniMax", "M2"),
("GPU", "RTX"),
]
recall_hits_5 = recall_hits_10 = 0
for query, expected in recall_tests:
r5 = api("POST", "/api/v1/recall", {"query": query, "limit": 5, "namespace": "shared"})
r10 = api("POST", "/api/v1/recall", {"query": query, "limit": 10, "namespace": "shared"})
hit_5 = any(expected in str(r.get("content", "")) for r in r5.get("results", []))
hit_10 = any(expected in str(r.get("content", "")) for r in r10.get("results", []))
if hit_5:
recall_hits_5 += 1
if hit_10:
recall_hits_10 += 1
print(f" '{query}' expected='{expected}': @5={'hit' if hit_5 else 'miss'}, @10={'hit' if hit_10 else 'miss'}")
recall_at_5 = recall_hits_5 / len(recall_tests)
recall_at_10 = recall_hits_10 / len(recall_tests)
print(f"Recall@5={recall_at_5:.4f}, Recall@10={recall_at_10:.4f}")
# === 2. Latency ===
print("\n=== Latency Test (100 reqs) ===")
endpoints = [
("POST", "/api/v1/recall", {"query": "牧尘", "limit": 5, "namespace": "shared"}),
("GET", "/api/v1/stats", None),
("GET", "/api/v1/graph/stats", None),
("POST", "/api/v1/graph/navigate", {"entity": "Docker", "max_hops": 2}),
]
lat_stats = {}
for method, path, data in endpoints:
# warmup
for _ in range(3):
time_request(method, path, data)
time.sleep(0.05)
lats = [time_request(method, path, data) for _ in range(30)]
sorted_lats = sorted(lats)
p50_idx = int(len(sorted_lats) * 0.5)
p95_idx = int(len(sorted_lats) * 0.95)
p99_idx = int(len(sorted_lats) * 0.99)
lat_stats[path] = {
"p50": sorted_lats[p50_idx],
"p95": sorted_lats[p95_idx],
"p99": sorted_lats[p99_idx]
}
print(f" {method} {path}: p50={lat_stats[path]['p50']:.1f}ms p95={lat_stats[path]['p95']:.1f}ms p99={lat_stats[path]['p99']:.1f}ms")
# === 3. Graph Navigation ===
print("\n=== Graph Navigation Test ===")
gstats = api("GET", "/api/v1/graph/stats")
nodes, edges = gstats.get("node_count", 0), gstats.get("edge_count", 0)
print(f"Graph: {nodes} nodes, {edges} edges")
entities = ["Docker", "Go", "Linux", "Arch", "Deepin", "RTX", "GPU", "Memory", "API", "Model"]
h1 = h2 = h3 = 0
for e in entities:
for hops in [1, 2, 3]:
r = api("POST", "/api/v1/graph/navigate", {"entity": e, "max_hops": hops})
time.sleep(0.05)
if r.get("paths"):
if hops == 1:
h1 += 1
elif hops == 2:
h2 += 1
else:
h3 += 1
trigger_rate = (h1 + h2 + h3) * 100 / 30
print(f"1-hop: {h1}/10, 2-hop: {h2}/10, 3-hop: {h3}/10, trigger rate: {trigger_rate:.1f}%")
# === 4. Concurrent Stability ===
print("\n=== Concurrent Stability Test (50 workers x 10 reqs) ===")
def worker():
ok = fail = 0
for i in range(10):
try:
api("POST", "/api/v1/recall", {"query": f"concurrent{i}", "limit": 5, "namespace": "shared"})
ok += 1
except:
fail += 1
return ok, fail
t0 = time.time()
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as ex:
results = list(ex.map(lambda _: worker(), range(50)))
t1 = time.time()
total_ok = sum(r[0] for r in results)
total_fail = sum(r[1] for r in results)
total_req = 500
success_rate = total_ok * 100 / total_req
throughput = total_req / (t1 - t0)
print(f"Success: {total_ok}/{total_req} ({success_rate:.2f}%), Fail: {total_fail}")
print(f"Duration: {t1-t0:.2f}s, Throughput: {throughput:.1f} req/s")
# Print results for extraction
print("\n=== RESULTS ===")
print(f"RECALL_5={recall_at_5:.4f}")
print(f"RECALL_10={recall_at_10:.4f}")
print(f"LAT_P50={lat_stats['/api/v1/recall']['p50']:.1f}")
print(f"LAT_P95={lat_stats['/api/v1/recall']['p95']:.1f}")
print(f"LAT_P99={lat_stats['/api/v1/recall']['p99']:.1f}")
print(f"GRAPH_NODES={nodes}")
print(f"GRAPH_EDGES={edges}")
print(f"GRAPH_TRIGGER={trigger_rate:.1f}")
print(f"CONCURRENT_OK={total_ok}")
print(f"CONCURRENT_TOTAL={total_req}")
print(f"CONCURRENT_RATE={success_rate:.2f}")
print(f"CONCURRENT_TP={throughput:.1f}")