整理代码并提交

This commit is contained in:
inliver233 committed 2026-02-22 18:58:55 +08:00
commit 968f37ad94
309 files changed
+46134

No files matched your search

+263
View File
@@ -0,0 +1,263 @@
from __future__ import annotations
import argparse
import asyncio
import json
import logging
import os
import platform
import random
import statistics
import sys
import tempfile
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
import httpx
REPO_ROOT = Path(__file__).resolve().parents[2]
BACKEND_DIR = REPO_ROOT / "backend"
if str(BACKEND_DIR) not in sys.path:
sys.path.insert(0, str(BACKEND_DIR))
from app.db.models.base import Base
from app.main import create_app
@dataclass(frozen=True, slots=True)
class BenchResult:
total: int
ok: int
no_match: int
other_error: int
durations_s: list[float]
def _p(sorted_values: list[float], p: float) -> float:
if not sorted_values:
return 0.0
p = max(0.0, min(float(p), 100.0))
if p == 0.0:
return sorted_values[0]
if p == 100.0:
return sorted_values[-1]
k = (len(sorted_values) - 1) * (p / 100.0)
f = int(k)
c = min(f + 1, len(sorted_values) - 1)
if f == c:
return sorted_values[f]
d0 = sorted_values[f] * (c - k)
d1 = sorted_values[c] * (k - f)
return d0 + d1
async def _migrate_and_seed(*, app, seed_images: int) -> None:
engine = app.state.engine
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
seed_images_i = max(1, int(seed_images))
values: list[tuple[Any, ...]] = []
for i in range(seed_images_i):
illust_id = 500_000_000 + i
random_key = (i + 0.5) / seed_images_i
values.append(
(
illust_id,
0,
"jpg",
f"https://i.pximg.net/img-original/img/2023/01/01/00/00/00/{illust_id}_p0.jpg",
f"/i/seed_{illust_id}.jpg",
float(random_key),
1000,
1000,
1.0,
3,
0,
0,
1,
0,
None,
None,
None,
None,
None,
None,
None,
None,
None,
now,
now,
)
)
await conn.exec_driver_sql(
"""
INSERT INTO images (
illust_id, page_index, ext, original_url, proxy_path, random_key,
width, height, aspect_ratio, orientation, x_restrict, ai_type,
status, fail_count, created_import_id,
last_fail_at, last_ok_at, last_error_code, last_error_msg,
user_id, user_name, title, created_at_pixiv,
added_at, updated_at
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);
""".strip(),
values,
)
async def _run_bench(*, app, total_requests: int, concurrency: int, endpoint: str) -> BenchResult:
total_i = max(1, int(total_requests))
conc_i = max(1, int(concurrency))
transport = httpx.ASGITransport(app=app)
timeout = httpx.Timeout(30.0, connect=10.0)
client = httpx.AsyncClient(transport=transport, base_url="http://bench.local", timeout=timeout)
semaphore = asyncio.Semaphore(conc_i)
durations_s: list[float] = []
ok = 0
no_match = 0
other_error = 0
async def one(i: int) -> None:
nonlocal ok, no_match, other_error
_ = i
async with semaphore:
started = time.perf_counter()
try:
resp = await client.get(endpoint)
if resp.status_code == 200:
ok += 1
elif resp.status_code == 404:
no_match += 1
else:
other_error += 1
except Exception:
other_error += 1
finally:
durations_s.append(time.perf_counter() - started)
try:
tasks = [asyncio.create_task(one(i)) for i in range(total_i)]
await asyncio.gather(*tasks)
finally:
await client.aclose()
return BenchResult(
total=total_i,
ok=ok,
no_match=no_match,
other_error=other_error,
durations_s=durations_s,
)
def _build_report(*, args, result: BenchResult, elapsed_s: float) -> dict[str, Any]:
ds = sorted(float(x) for x in result.durations_s if x is not None and x >= 0)
mean = statistics.fmean(ds) if ds else 0.0
p50 = _p(ds, 50.0)
p90 = _p(ds, 90.0)
p99 = _p(ds, 99.0)
report: dict[str, Any] = {
"created_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
"endpoint": args.endpoint,
"requests": {
"total": result.total,
"concurrency": args.concurrency,
"ok": result.ok,
"no_match": result.no_match,
"other_error": result.other_error,
},
"latency_s": {
"min": ds[0] if ds else 0.0,
"p50": p50,
"p90": p90,
"p99": p99,
"max": ds[-1] if ds else 0.0,
"mean": mean,
},
"throughput": {
"elapsed_s": float(elapsed_s),
"rps": float(result.total / elapsed_s) if elapsed_s > 0 else 0.0,
},
"seed_images": int(args.seed_images),
"env": {
"python": sys.version.split()[0],
"platform": platform.platform(),
"machine": platform.machine(),
},
}
return report
async def main_async(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="In-process /random load test using httpx ASGITransport.")
parser.add_argument("--requests", type=int, default=500)
parser.add_argument("--concurrency", type=int, default=50)
parser.add_argument("--seed-images", type=int, default=5000)
parser.add_argument("--endpoint", type=str, default="/random?format=json&attempts=1")
parser.add_argument("--output", type=str, default="")
args = parser.parse_args(argv)
with tempfile.TemporaryDirectory(prefix="new_pixiv_bench_") as td:
db_path = Path(td) / "bench.db"
db_url = "sqlite+aiosqlite:///" + db_path.as_posix()
os.environ["APP_ENV"] = os.environ.get("APP_ENV") or "dev"
os.environ["DATABASE_URL"] = db_url
os.environ["SECRET_KEY"] = os.environ.get("SECRET_KEY") or "dev-secret-key"
os.environ["ADMIN_USERNAME"] = os.environ.get("ADMIN_USERNAME") or "admin"
app = create_app()
try:
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("httpcore").setLevel(logging.WARNING)
await _migrate_and_seed(app=app, seed_images=args.seed_images)
# Warm-up: one request to prime import paths/DB connections.
async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app),
base_url="http://bench.local",
) as c:
await c.get("/healthz")
started = time.perf_counter()
result = await _run_bench(
app=app,
total_requests=args.requests,
concurrency=args.concurrency,
endpoint=args.endpoint,
)
elapsed_s = time.perf_counter() - started
report = _build_report(args=args, result=result, elapsed_s=elapsed_s)
out_path = (args.output or "").strip()
if out_path:
out = Path(out_path)
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
print(f"[bench_random_asgi] wrote report: {out}")
else:
print(json.dumps(report, ensure_ascii=False, indent=2))
finally:
engine = getattr(app.state, "engine", None)
if engine is not None:
await engine.dispose()
return 0
def main(argv: list[str] | None = None) -> int:
return asyncio.run(main_async(argv))
if __name__ == "__main__":
raise SystemExit(main())
+21
View File
@@ -0,0 +1,21 @@
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
Set-Location $repoRoot
Write-Host "== test_all: backend =="
& (Join-Path $PSScriptRoot "test_backend.ps1")
Write-Host "== test_all: frontend =="
& (Join-Path $PSScriptRoot "test_frontend.ps1")
if (Test-Path "deploy\\docker-compose.yml") {
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
throw "docker not found but deploy/docker-compose.yml exists"
}
Write-Host "== test_all: docker compose config =="
docker compose -f deploy/docker-compose.yml config | Out-Null
}
Write-Host "== test_all: OK =="
+22
View File
@@ -0,0 +1,22 @@
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
Set-Location $repoRoot
if (-not (Test-Path "backend")) {
Write-Host "[test_backend] backend/ not found -> skip"
exit 0
}
Write-Host "[test_backend] python compileall"
py -3.11 -m compileall -q backend
if (Test-Path "backend\\tests") {
Write-Host "[test_backend] pytest"
py -3.11 -m pytest -q backend\\tests
} else {
Write-Host "[test_backend] backend/tests not found -> skip pytest"
}
Write-Host "[test_backend] OK"
+32
View File
@@ -0,0 +1,32 @@
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
Set-Location $repoRoot
if (-not (Test-Path "frontend\\package.json")) {
Write-Host "[test_frontend] frontend/package.json not found -> skip"
exit 0
}
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
throw "npm not found but frontend/package.json exists"
}
Write-Host "[test_frontend] npm -C frontend run lint (if present)"
try { npm -C frontend run lint } catch { Write-Host "[test_frontend] lint missing or failed: $($_.Exception.Message)"; throw }
Write-Host "[test_frontend] npm -C frontend run typecheck (if present)"
try { npm -C frontend run typecheck } catch { Write-Host "[test_frontend] typecheck missing or failed: $($_.Exception.Message)"; throw }
Write-Host "[test_frontend] npm -C frontend test (if present)"
if (-not $env:VITEST_MAX_THREADS) { $env:VITEST_MAX_THREADS = "1" }
if (-not $env:VITEST_MIN_THREADS) { $env:VITEST_MIN_THREADS = "1" }
Write-Host "[test_frontend] vitest threads: min=$($env:VITEST_MIN_THREADS) max=$($env:VITEST_MAX_THREADS)"
try { npm -C frontend test } catch { Write-Host "[test_frontend] test missing or failed: $($_.Exception.Message)"; throw }
Write-Host "[test_frontend] npm -C frontend run build (if present)"
if (-not $env:VITE_BUILD_MINIFY) { $env:VITE_BUILD_MINIFY = "false" }
Write-Host "[test_frontend] vite build minify=$($env:VITE_BUILD_MINIFY)"
try { npm -C frontend run build } catch { Write-Host "[test_frontend] build missing or failed: $($_.Exception.Message)"; throw }
Write-Host "[test_frontend] OK"
+36
View File
@@ -0,0 +1,36 @@
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
Set-Location $repoRoot
if (-not (Test-Path "requirements-dev.txt")) {
throw "requirements-dev.txt not found at repo root"
}
if (-not (Test-Path "backend")) {
throw "backend/ not found"
}
$venvRoot = Join-Path $repoRoot ".venv"
$venvPath = Join-Path $venvRoot "backend_verify"
if (Test-Path $venvPath) {
Remove-Item -Recurse -Force $venvPath
}
Write-Host "[verify_backend_install] create venv: $venvPath"
py -3.11 -m venv $venvPath
$python = Join-Path $venvPath "Scripts\\python.exe"
Write-Host "[verify_backend_install] pip install -U pip"
& $python -m pip install -U pip
Write-Host "[verify_backend_install] pip install -r requirements-dev.txt"
& $python -m pip install -r requirements-dev.txt
Write-Host "[verify_backend_install] pytest"
& $python -m pytest -q backend\\tests
Write-Host "[verify_backend_install] OK"