154 lines
5.2 KiB
Python
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}") |