feat(task): 批量改状态与看板数据接口
- POST /items/batch-status:逐条成败,不做全回滚;拖 10 条里 1 条冲突不该让另 9 条白干
- GET /items/board:四列一次取全,收件箱不进看板
- 修:/items/board 被 /{item_id} 抢先匹配导致 422
- 新增向量库配置项(M3 用,独立实例,不动 fnsvr-01)
- 3 个新测试,全量 46 passed
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"""四视图 + 看板的首屏耗时实测(1000 条任务)。
|
||||
|
||||
不是压测工具,只回答一个问题:**1000 条之后,点开还会不会卡**。
|
||||
跑之前先起后端:`uvicorn app.main:app --port 8100`(连开发库)。
|
||||
|
||||
python scripts/bench_views.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import statistics
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import httpx
|
||||
|
||||
BASE = "http://127.0.0.1:8100/api/v1"
|
||||
TOTAL = 1000
|
||||
REPEAT = 15
|
||||
|
||||
|
||||
async def login(client: httpx.AsyncClient) -> tuple[str, dict[str, str]]:
|
||||
phone = f"137{uuid.uuid4().int % 100_000_000:08d}"
|
||||
sent = await client.post(f"{BASE}/auth/sms/send", json={"phone": phone})
|
||||
code = sent.json()["data"]["dev_code"]
|
||||
out = await client.post(f"{BASE}/auth/login", json={"phone": phone, "code": code})
|
||||
token = out.json()["data"]["tokens"]["access_token"]
|
||||
return token, {"Authorization": f"Bearer {token}"}
|
||||
|
||||
|
||||
async def main() -> None:
|
||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||
_, headers = await login(client)
|
||||
ws = await client.post(f"{BASE}/workspaces", json={"name": "性能实测"}, headers=headers)
|
||||
ws_id = ws.json()["data"]["id"]
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
statuses = ["todo", "doing", "blocked", "done", "inbox"]
|
||||
print(f"造 {TOTAL} 条任务……")
|
||||
started = time.perf_counter()
|
||||
for index in range(TOTAL):
|
||||
payload = {
|
||||
"title": f"测试任务 {index:04d} " + ("周报" if index % 7 == 0 else "跟进"),
|
||||
"status": statuses[index % len(statuses)],
|
||||
"priority": index % 4,
|
||||
"due_at": (now + timedelta(days=(index % 10) - 5)).isoformat(),
|
||||
"tag_names": ["批量", f"组{index % 5}"],
|
||||
}
|
||||
await client.post(f"{BASE}/workspaces/{ws_id}/items", json=payload, headers=headers)
|
||||
print(f"写入耗时 {time.perf_counter() - started:.1f}s")
|
||||
|
||||
targets = {
|
||||
"今天": f"/workspaces/{ws_id}/items?view=today&limit=30",
|
||||
"进行中": f"/workspaces/{ws_id}/items?view=active&limit=30",
|
||||
"收件箱": f"/workspaces/{ws_id}/items?view=inbox&limit=30",
|
||||
"全部": f"/workspaces/{ws_id}/items?view=all&limit=30",
|
||||
"搜索(周报)": f"/workspaces/{ws_id}/items?view=all&q=%E5%91%A8%E6%8A%A5&limit=30",
|
||||
"看板": f"/workspaces/{ws_id}/items/board",
|
||||
}
|
||||
|
||||
print("\n| 视图 | 首次 | P50 | P95 | 返回条数 |")
|
||||
print("|---|---|---|---|---|")
|
||||
for label, path in targets.items():
|
||||
samples: list[float] = []
|
||||
count = 0
|
||||
for attempt in range(REPEAT):
|
||||
t0 = time.perf_counter()
|
||||
resp = await client.get(f"{BASE}{path}", headers=headers)
|
||||
samples.append((time.perf_counter() - t0) * 1000)
|
||||
if attempt == 0:
|
||||
body = resp.json()["data"]
|
||||
if "columns" in body:
|
||||
count = sum(len(v) for v in body["columns"].values())
|
||||
else:
|
||||
count = len(body["items"])
|
||||
samples.sort()
|
||||
p50 = statistics.median(samples)
|
||||
p95 = samples[min(len(samples) - 1, int(len(samples) * 0.95))]
|
||||
print(f"| {label} | {samples[0]:.1f} ms | {p50:.1f} ms | {p95:.1f} ms | {count} |")
|
||||
|
||||
print(
|
||||
"\n注:含网络往返与鉴权中间件;本机 127.0.0.1,所以这是**下限**,"
|
||||
"真实用户还要加速度。"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user