#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ devperf_bench.py Mac Studio (M2 Ultra) 开发链路网络性能基准测试 + 专业UI - 统一代理:127.0.0.1:7890(ClashX) - 任务:brew / mvn / docker / kubectl / helm / pip / git - 指标:latency / bytes / throughput / success - UI:Chart.js + Tailwind,一键可视化 """ import asyncio, os, sys, json, time, shutil, tempfile, hashlib, psutil, pathlib from typing import List, Dict, Any, Optional from fastapi import FastAPI, BackgroundTasks, Query from fastapi.responses import HTMLResponse, JSONResponse, PlainTextResponse # ========================= # 全局配置(可自行调整) # ========================= PROXY_HTTP = os.environ.get("http_proxy", "http://127.0.0.1:7890") PROXY_HTTPS = os.environ.get("https_proxy", "http://127.0.0.1:7890") # 测试对象(使用更大型的包/镜像/仓库进行专业测试) CFG = { "concurrency": 4, # 并发度 "repeat": 1, # 每类任务重复次数 "workdir": ".devperf_tmp", # 工作目录 "targets": { "brew": { "desc": "brew 拉取大型 bottle(用下载替代)", # 使用 brew 下载一个较大型的包(不会安装到系统,尽量不污染环境) "cmd": "brew fetch --force --retry --quiet --deps node python docker", "size_probe": "du -sb $(brew --cache) | head -n 1 | cut -f1" }, "mvn": { "desc": "maven 预拉大型项目依赖(Spring Boot Web Starter)", "cmd": "mvn -q -Dmaven.repo.local=.m2_repo -f pom.xml dependency:go-offline", "bootstrap": { "pom": """ 4.0.0 benchbench1.0 jar 17 17 org.springframework.boot spring-boot-starter-web 3.2.0 org.springframework.boot spring-boot-starter-data-jpa 3.2.0 com.fasterxml.jackson.core jackson-databind 2.15.2 org.hibernate hibernate-core 6.3.1.Final """ }, "size_dir": ".m2_repo" # 以目录体积估算 }, "docker": { "desc": "docker 拉取大型镜像(Node.js + Python + Java 环境)", "images": ["node:21-alpine", "python:3.12-slim", "openjdk:17-jdk-slim", "postgres:16-alpine"], "cmd_tpl": "docker pull {image}" }, "kubectl": { "desc": "k8s 组件镜像(用 docker pull 官方镜像代替触达)", "images": [ "registry.k8s.io/kube-apiserver:v1.28.0", "registry.k8s.io/kube-controller-manager:v1.28.0", "registry.k8s.io/kube-scheduler:v1.28.0", "registry.k8s.io/coredns/coredns:v1.10.1" ], "cmd_tpl": "docker pull {image}" }, "helm": { "desc": "helm 拉取大型 chart(Kubernetes Dashboard + Prometheus)", "init": [ "helm repo add stable https://charts.helm.sh/stable", "helm repo add prometheus-community https://prometheus-community.github.io/helm-charts", "helm repo update" ], "pull": "helm pull prometheus-community/kube-prometheus --destination . && helm pull stable/kubernetes-dashboard --destination ." }, "pip": { "desc": "pip 下载大型库(Django, FastAPI, Pandas等)", "cmd": "pip download --no-deps --dest . 'django>=4.2' fastapi uvicorn pandas numpy requests pydantic", "size_glob": "*.whl" }, "git": { "desc": "GitHub 克隆大型开源项目(浅克隆)", "repos": [ "https://github.com/microsoft/vscode.git", "https://github.com/torvalds/linux.git", "https://github.com/facebook/react.git", "https://github.com/angular/angular.git" ], "cmd_tpl": "git clone --depth 1 --single-branch {repo}" } } } RESULTS: List[Dict[str, Any]] = [] RUNNING = False # ============ 工具函数 ============ def human(n: float) -> str: for u in ["B","KB","MB","GB","TB"]: if n < 1024: return f"{n:.2f} {u}" n /= 1024 return f"{n:.2f} PB" async def run_cmd(cmd: str, cwd: Optional[str]=None, env: Optional[dict]=None, timeout: int=0) -> Dict[str, Any]: """运行命令并返回 {rc, out, err, elapsed}""" start = time.time() try: proc = await asyncio.create_subprocess_shell( cmd, cwd=cwd, env=env or os.environ.copy(), stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) out, err = await asyncio.wait_for(proc.communicate(), timeout=timeout or None) elapsed = time.time() - start return {"rc": proc.returncode, "out": out.decode(errors="ignore"), "err": err.decode(errors="ignore"), "elapsed": elapsed} except asyncio.TimeoutError: return {"rc": -1, "out": "", "err": f"timeout after {timeout}s", "elapsed": time.time() - start} def dir_size_bytes(path: str) -> int: total = 0 p = pathlib.Path(path) if not p.exists(): return 0 for f in p.rglob("*"): if f.is_file(): try: total += f.stat().st_size except: pass return total def glob_total_bytes(glob_pattern: str, cwd: str) -> int: total = 0 for f in pathlib.Path(cwd).glob(glob_pattern): if f.is_file(): try: total += f.stat().st_size except: pass return total def record(task: str, label: str, bytes_n: int, elapsed: float, rc: int, extra: Dict[str, Any]=None): thr = (bytes_n/elapsed/1024/1024) if (bytes_n>0 and elapsed>0) else 0.0 RESULTS.append({ "ts": time.strftime("%Y-%m-%d %H:%M:%S"), "task": task, "label": label, "bytes": bytes_n, "elapsed_sec": round(elapsed, 3), "throughput_mbps": round(thr, 2), "success": (rc==0), **(extra or {}) }) # ============ 每类任务的执行器 ============ async def bench_brew(base: str): td = pathlib.Path(base, "brew"); td.mkdir(parents=True, exist_ok=True) # 清理缓存项以避免复用(可选) # await run_cmd("brew cleanup --prune=all", cwd=str(td)) res = await run_cmd(CFG["targets"]["brew"]["cmd"], cwd=str(td)) size_res = await run_cmd(CFG["targets"]["brew"]["size_probe"], cwd=str(td)) bytes_n = int(size_res["out"].strip()) if size_res["rc"]==0 and size_res["out"].strip().isdigit() else 0 record("brew", "node+python+docker", bytes_n, res["elapsed"], res["rc"], {"stderr": res["err"][-200:]}) async def bench_mvn(base: str): td = pathlib.Path(base, "mvn"); td.mkdir(parents=True, exist_ok=True) (td / "pom.xml").write_text(CFG["targets"]["mvn"]["bootstrap"]["pom"], encoding="utf-8") # 先清目录 shutil.rmtree(td / ".m2_repo", ignore_errors=True) res = await run_cmd(CFG["targets"]["mvn"]["cmd"], cwd=str(td)) bytes_n = dir_size_bytes(str(td / ".m2_repo")) record("mvn", "spring-boot-starter-web", bytes_n, res["elapsed"], res["rc"], {"stderr": res["err"][-200:]}) async def bench_docker(base: str): td = pathlib.Path(base, "docker"); td.mkdir(parents=True, exist_ok=True) for img in CFG["targets"]["docker"]["images"]: res = await run_cmd(CFG["targets"]["docker"]["cmd_tpl"].format(image=img), cwd=str(td)) # 用 inspect 拿镜像大小 insp = await run_cmd(f"docker image inspect {img} --format='{{{{.Size}}}}'") bytes_n = int(insp["out"].strip()) if insp["rc"]==0 and insp["out"].strip().isdigit() else 0 record("docker", img, bytes_n, res["elapsed"], res["rc"], {"stderr": res["err"][-200:]}) async def bench_kubectl(base: str): td = pathlib.Path(base, "kubectl"); td.mkdir(parents=True, exist_ok=True) for img in CFG["targets"]["kubectl"]["images"]: res = await run_cmd(CFG["targets"]["kubectl"]["cmd_tpl"].format(image=img), cwd=str(td)) insp = await run_cmd(f"docker image inspect {img} --format='{{{{.Size}}}}'") bytes_n = int(insp["out"].strip()) if insp["rc"]==0 and insp["out"].strip().isdigit() else 0 record("k8s-image", img, bytes_n, res["elapsed"], res["rc"], {"stderr": res["err"][-200:]}) async def bench_helm(base: str): td = pathlib.Path(base, "helm"); td.mkdir(parents=True, exist_ok=True) # 初始化仓库 for cmd in CFG["targets"]["helm"]["init"]: await run_cmd(cmd, cwd=str(td)) # 删除旧文件,避免缓存干扰 for f in pathlib.Path(td).glob("*.tgz"): f.unlink(missing_ok=True) res = await run_cmd(CFG["targets"]["helm"]["pull"], cwd=str(td)) bytes_n = glob_total_bytes("*.tgz", str(td)) record("helm", "prometheus+k8s-dashboard", bytes_n, res["elapsed"], res["rc"], {"stderr": res["err"][-200:]}) async def bench_pip(base: str): td = pathlib.Path(base, "pip"); td.mkdir(parents=True, exist_ok=True) # 清空旧轮子 for f in pathlib.Path(td).glob("*.whl"): f.unlink(missing_ok=True) res = await run_cmd(CFG["targets"]["pip"]["cmd"], cwd=str(td)) bytes_n = glob_total_bytes("*.whl", str(td)) record("pip", "django+fastapi+pandas", bytes_n, res["elapsed"], res["rc"], {"stderr": res["err"][-200:]}) async def bench_git(base: str): td = pathlib.Path(base, "git"); td.mkdir(parents=True, exist_ok=True) for repo in CFG["targets"]["git"]["repos"]: name = repo.rsplit("/",1)[-1].replace(".git", "") dst = pathlib.Path(td, name) if dst.exists(): shutil.rmtree(dst, ignore_errors=True) res = await run_cmd(CFG["targets"]["git"]["cmd_tpl"].format(repo=repo), cwd=str(td)) bytes_n = dir_size_bytes(str(dst)) record("git", name, bytes_n, res["elapsed"], res["rc"], {"stderr": res["err"][-200:]}) TASK_FUNCS = { "brew": bench_brew, "mvn": bench_mvn, "docker": bench_docker, "kubectl": bench_kubectl, "helm": bench_helm, "pip": bench_pip, "git": bench_git } # ============ 调度器 ============ async def run_all(concurrency: int, repeat: int): global RUNNING if RUNNING: return RUNNING = True RESULTS.clear() base = CFG["workdir"] shutil.rmtree(base, ignore_errors=True) pathlib.Path(base).mkdir(parents=True, exist_ok=True) # 确保代理在环境变量中 os.environ["http_proxy"] = PROXY_HTTP os.environ["https_proxy"] = PROXY_HTTPS # 构建任务队列 jobs = [] for _ in range(repeat): for key in TASK_FUNCS.keys(): jobs.append((key, TASK_FUNCS[key])) sem = asyncio.Semaphore(concurrency) async def wrapper(name, coro): async with sem: try: await coro(CFG["workdir"]) except Exception as e: record(name, "exception", 0, 0.0, -1, {"exception": str(e)}) await asyncio.gather(*[wrapper(n, f) for n,f in jobs]) # 写入结果 pathlib.Path(base, "results.json").write_text(json.dumps(RESULTS, ensure_ascii=False, indent=2), encoding="utf-8") RUNNING = False # ============ FastAPI + UI ============ app = FastAPI(title="DevPerf Bench (Mac Studio M2 Ultra)") @app.get("/", response_class=HTMLResponse) def index(): return HTMLResponse(""" DevPerf Bench · M2 Ultra

DevPerf Bench · Mac Studio M2 Ultra

并发度
重复
Idle

吞吐对比(平均值)

明细

时间 任务 标签 传输 时延 吞吐 状态
提示:结果已落盘 .devperf_tmp/results.json;你可改 CFG 里的镜像/仓库以贴合你的真实项目。
""") @app.get("/run") async def run(concurrency: int = Query(CFG["concurrency"], ge=1, le=64), repeat: int = Query(CFG["repeat"], ge=1, le=10), background_tasks: BackgroundTasks = None): """后台启动一次完整测试""" if RUNNING: return JSONResponse({"ok": False, "msg": "already running"}) background_tasks.add_task(run_all, concurrency, repeat) return JSONResponse({"ok": True, "msg": "started"}) @app.get("/results") def results(): return JSONResponse(RESULTS) @app.get("/raw") def raw(): """导出 JSON(便于你后续喂给 Grafana/ELK)""" return PlainTextResponse(json.dumps(RESULTS, ensure_ascii=False, indent=2)) if __name__ == "__main__": import uvicorn print(f"[i] Using proxy http={PROXY_HTTP} https={PROXY_HTTPS}") uvicorn.run(app, host="127.0.0.1", port=8000)