#!/usr/bin/env python3 """基准测试:19张图片 × 5种发际线,记录每步耗时和显存变化。 用法: python3 benchmark_grow.py 输出: benchmark_results.json + benchmark_report.html """ import json, os, time, base64, subprocess, re, glob, shutil from datetime import datetime from pathlib import Path import urllib.request, urllib.error API_URL = "http://127.0.0.1:8187/api/v1/hair/grow" TOKEN = "dev-shared-secret-2026" IMG_DIR = "/home/ubuntu/hair/image/girl_img" OUT_DIR = "/home/ubuntu/hair/benchmark_out" RESULT_JSON = os.path.join(OUT_DIR, "benchmark_results.json") REPORT_HTML = os.path.join(OUT_DIR, "benchmark_report.html") HAIRLINES = [ ("1", "ellipse", "椭圆形"), ("2", "flower", "花瓣形"), ("3", "heart", "心形"), ("4", "straight", "直线形"), ("5", "wave", "波浪形"), ] os.makedirs(OUT_DIR, exist_ok=True) # ── 工具函数 ────────────────────────────────────────────── def get_vram(): """返回 (used_MB, free_MB)""" try: out = subprocess.check_output( ["nvidia-smi", "--query-gpu=memory.used,memory.free", "--format=csv,noheader,nounits"], text=True, timeout=5 ).strip() used, free = out.split(",") return int(used.strip()), int(free.strip()) except Exception: return -1, -1 def get_gpu_procs(): """返回各进程显存占用 dict""" try: out = subprocess.check_output( ["nvidia-smi", "--query-compute-apps=pid,used_memory", "--format=csv,noheader,nounits"], text=True, timeout=5 ).strip() procs = {} for line in out.splitlines(): parts = line.split(",") if len(parts) >= 2: procs[parts[0].strip()] = int(parts[1].strip()) return procs except Exception: return {} def read_worker_log_tail(n=50): """读取 worker.log 最后 n 行""" log_path = "/home/ubuntu/hair/worker.log" try: result = subprocess.run(["tail", "-n", str(n), log_path], capture_output=True, text=True, timeout=5) return result.stdout except Exception: return "" def call_api(image_path, hair_style_value): """调用接口2,返回 (json_dict, elapsed_sec, error_or_None)""" import mimetypes boundary = "----BenchmarkBoundary" + str(int(time.time()*1000)) filename = os.path.basename(image_path) mime = mimetypes.guess_type(image_path)[0] or "image/jpeg" with open(image_path, "rb") as f: img_data = f.read() body_parts = [] body_parts.append(f"--{boundary}\r\n".encode()) body_parts.append(f'Content-Disposition: form-data; name="image_file"; filename="{filename}"\r\n'.encode()) body_parts.append(f"Content-Type: {mime}\r\n\r\n".encode()) body_parts.append(img_data) body_parts.append(f"\r\n--{boundary}\r\n".encode()) body_parts.append(b'Content-Disposition: form-data; name="gender"\r\n\r\n') body_parts.append(b"female") body_parts.append(f"\r\n--{boundary}\r\n".encode()) body_parts.append(b'Content-Disposition: form-data; name="hair_style"\r\n\r\n') body_parts.append(hair_style_value.encode()) body_parts.append(f"\r\n--{boundary}\r\n".encode()) body_parts.append(b'Content-Disposition: form-data; name="use_mask"\r\n\r\n') body_parts.append(b"0") body_parts.append(f"\r\n--{boundary}\r\n".encode()) body_parts.append(b'Content-Disposition: form-data; name="prompt"\r\n\r\n') body_parts.append(b"\xe5\xa1\xab\xe5\x85\x85\xe9\x81\xae\xe7\xbd\xa9\xe5\x8c\xba\xe5\x9f\x9f\xe7\x9a\x84\xe5\xa4\xb4\xe5\x8f\x91\xef\xbc\x8c\xe7\x9a\xae\xe8\x82\xa4\xe5\x8a\xa0\xe4\xb8\x80\xe7\x82\xb9\xe7\xa3\xa8\xe7\x9a\xae") body_parts.append(f"\r\n--{boundary}--\r\n".encode()) body = b"".join(body_parts) req = urllib.request.Request(API_URL, data=body, method="POST") req.add_header("Content-Type", f"multipart/form-data; boundary={boundary}") req.add_header("X-Internal-Token", TOKEN) t0 = time.time() try: with urllib.request.urlopen(req, timeout=600) as resp: raw = resp.read() elapsed = time.time() - t0 data = json.loads(raw) return data, elapsed, None except urllib.error.HTTPError as e: elapsed = time.time() - t0 try: data = json.loads(e.read()) except Exception: data = {"error": str(e)} return data, elapsed, f"HTTP {e.code}" except Exception as e: elapsed = time.time() - t0 return {}, elapsed, str(e) def parse_step_timing(log_text): """从 worker.log 文本中解析步骤耗时""" timing = {} for line in log_text.splitlines(): # 匹配 "步骤1 遮罩完成 耗时=949ms" m = re.search(r"步骤(\d+)\s+\S+\s+耗时=(\d+)ms", line) if m: timing[f"step{m.group(1)}_ms"] = int(m.group(2)) # 匹配 "人脸检出 px_per_cm=30.669 图尺寸=813x967" if "人脸检出" in line: m2 = re.search(r"px_per_cm=([\d.]+)", line) if m2: timing["px_per_cm"] = float(m2.group(1)) # 匹配 "头发分割完成" if "头发分割完成" in line: timing["hair_seg"] = True # 匹配 "换发型图失败" if "换发型图失败" in line or "换发型服务不可达" in line: timing["swap_error"] = line.strip()[-100:] # 匹配 "重绘" 相关 if "重绘" in line and ("完成" in line or "失败" in line): timing["redraw_status"] = "完成" if "完成" in line else "失败" return timing # ── 主流程 ──────────────────────────────────────────────── def run_benchmark(): images = sorted(glob.glob(os.path.join(IMG_DIR, "girl*.jpg"))) print(f"找到 {len(images)} 张图片") # 加载已有结果(支持断点续跑) results = [] if os.path.exists(RESULT_JSON): with open(RESULT_JSON) as f: results = json.load(f) print(f"已有 {len(results)} 条记录,继续未完成的测试") total_calls = len(images) * len(HAIRLINES) done = len(results) print(f"总计 {total_calls} 次调用,已完成 {done},剩余 {total_calls - done}") log_offset = 0 try: log_offset = subprocess.check_output(["wc", "-l", "/home/ubuntu/hair/worker.log"], text=True, timeout=5).split()[0] log_offset = int(log_offset) except Exception: pass for img_idx, img_path in enumerate(images): img_name = os.path.basename(img_path) # 跳过已完成的图片 img_results = [r for r in results if r["image"] == img_name] if len(img_results) >= len(HAIRLINES): print(f"[{img_idx+1}/{len(images)}] {img_name} 已完成,跳过") continue for hl_id, hl_key, hl_label in HAIRLINES: # 跳过已完成的 existing = [r for r in results if r["image"] == img_name and r["hairline_id"] == hl_id] if existing: continue print(f"\n[{img_idx+1}/{len(images)}] {img_name} → {hl_label}({hl_id}) ...") # 记录 worker.log 行数 try: log_before = int(subprocess.check_output( ["wc", "-l", "/home/ubuntu/hair/worker.log"], text=True, timeout=5).split()[0]) except Exception: log_before = 0 # VRAM before vram_before, vram_free_before = get_vram() procs_before = get_gpu_procs() t_start = time.time() # 调用API api_result, elapsed, error = call_api(img_path, hl_id) t_end = time.time() # VRAM after vram_after, vram_free_after = get_vram() procs_after = get_gpu_procs() # 读取新日志 try: log_diff = subprocess.check_output( ["tail", "-n", "+{}".format(log_before + 1), "/home/ubuntu/hair/worker.log"], text=True, timeout=5) except Exception: log_diff = "" step_timing = parse_step_timing(log_diff) # 提取结果图片 preview_b64 = "" grown_b64 = "" api_code = api_result.get("code", -1) api_msg = api_result.get("message", "") api_results = api_result.get("data", {}).get("results", []) if api_results: r0 = api_results[0] preview_b64 = r0.get("image_base64", "") grown_b64 = r0.get("grown_image_base64", "") # 保存缩略图 thumb_dir = os.path.join(OUT_DIR, "thumbs") os.makedirs(thumb_dir, exist_ok=True) if grown_b64: grown_bytes = base64.b64decode(grown_b64) thumb_path = os.path.join(thumb_dir, f"{img_name}_hl{hl_id}_grown.jpg") with open(thumb_path, "wb") as f: f.write(grown_bytes) if preview_b64: preview_bytes = base64.b64decode(preview_b64) thumb_path = os.path.join(thumb_dir, f"{img_name}_hl{hl_id}_preview.png") with open(thumb_path, "wb") as f: f.write(preview_bytes) record = { "image": img_name, "image_idx": img_idx + 1, "hairline_id": hl_id, "hairline_key": hl_key, "hairline_label": hl_label, "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "total_time_s": round(elapsed, 2), "api_code": api_code, "api_msg": api_msg, "error": error, "has_preview": bool(preview_b64), "has_grown": bool(grown_b64), "preview_size": len(preview_b64), "grown_size": len(grown_b64), "vram_before_mb": vram_before, "vram_after_mb": vram_after, "vram_free_before_mb": vram_free_before, "vram_free_after_mb": vram_free_after, "vram_delta_mb": vram_after - vram_before, "procs_before": procs_before, "procs_after": procs_after, "step_timing": step_timing, } results.append(record) print(f" → HTTP code={api_code} time={elapsed:.1f}s vram={vram_before}→{vram_after}MB " f"preview={'✓' if preview_b64 else '✗'} grown={'✓' if grown_b64 else '✗'}") # 保存中间结果 with open(RESULT_JSON, "w") as f: json.dump(results, f, indent=2, ensure_ascii=False) # 生成中间报告 generate_html(results) # 最终报告 generate_html(results) print(f"\n✅ 完成!结果: {RESULT_JSON}") print(f"📄 报告: {REPORT_HTML}") print(f"📊 总调用: {len(results)}/{total_calls}") def generate_html(results): """生成HTML报告""" # 统计 total = len(results) success = sum(1 for r in results if r["has_grown"]) failed = total - success times = [r["total_time_s"] for r in results if r["has_grown"]] avg_time = sum(times) / len(times) if times else 0 max_time = max(times) if times else 0 min_time = min(times) if times else 0 # 按发际线类型分组统计 hl_stats = {} for r in results: if r["has_grown"]: hl = r["hairline_label"] if hl not in hl_stats: hl_stats[hl] = {"count": 0, "times": []} hl_stats[hl]["count"] += 1 hl_stats[hl]["times"].append(r["total_time_s"]) # 按图片分组 img_groups = {} for r in results: img = r["image"] if img not in img_groups: img_groups[img] = [] img_groups[img].append(r) # 生成VRAM变化数据 vram_data = [(i, r["vram_after_mb"]) for i, r in enumerate(results)] html = f""" 接口2 基准测试报告

📊 接口2 基准测试报告

生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | {total} 次调用
{success}
成功
{failed}
失败
{avg_time:.1f}s
平均耗时
{min_time:.1f}s
最快
{max_time:.1f}s
最慢
""" # 按发际线类型统计 if hl_stats: html += '
按发际线类型统计
' for hl_label in ["椭圆形","花瓣形","心形","直线形","波浪形"]: if hl_label in hl_stats: s = hl_stats[hl_label] times = s["times"] avg = sum(times) / len(times) mn, mx = min(times), max(times) cls = hl_label html += f'
{hl_label}
{avg:.1f}s
{mn:.1f}~{mx:.1f}s ({s["count"]}次)
' else: html += f'
{hl_label}
-
未完成
' html += '
' # VRAM 变化图 if vram_data: max_vram = max(v for _, v in vram_data if v > 0) or 1 html += '
显存变化
' for i, (_, vram) in enumerate(vram_data): if vram > 0: h = int(vram / max_vram * 100) html += f'
' else: html += f'
' html += '
' # 详细结果表格 html += '
详细结果
' html += '' html += '' html += '' html += '' for i, r in enumerate(results): hl_cls = r["hairline_key"] status = '✓ 成功' if r["has_grown"] else f'✗ {r.get("error","")}' step_ms = r.get("step_timing", {}).get("step1_ms", "") step_str = f"{step_ms}ms" if step_ms else "-" vram_d = r["vram_delta_mb"] vram_d_str = f'0 else "#0f0"}">{"+" if vram_d>=0 else ""}{vram_d}' # 缩略图 thumb_grown = "" if r["has_grown"]: thumb_path = f"thumbs/{r['image']}_hl{r['hairline_id']}_grown.jpg" if os.path.exists(os.path.join(OUT_DIR, thumb_path)): thumb_grown = f'' html += f"""""" html += '
#图片发际线总耗时遮罩步骤显存前显存后显存变化预览图生发图状态
{i+1} {r['image']} {r['hairline_label']}
{r['total_time_s']:.1f}s
{step_str} {r['vram_before_mb']}MB {r['vram_after_mb']}MB {vram_d_str} {'✓' if r['has_preview'] else '✗'} {thumb_grown if thumb_grown else ('✓' if r['has_grown'] else '✗')} {status}
' html += '' with open(REPORT_HTML, "w", encoding="utf-8") as f: f.write(html) if __name__ == "__main__": run_benchmark()