From 2d889c80e06abe326757168633f910f5f6355fab Mon Sep 17 00:00:00 2001 From: hz4th_coder Date: Mon, 31 Aug 2026 00:17:26 +0800 Subject: [PATCH] =?UTF-8?q?v2.2.0=EF=BC=9Adata-chart-tool=20=E5=8F=8CY?= =?UTF-8?q?=E8=BD=B4=E6=8A=98=E7=BA=BF=E5=9B=BE=EF=BC=88=E9=A2=84=E5=A1=AB?= =?UTF-8?q?=E5=85=85=E5=B7=A6=E8=BD=B4=E8=99=9A=E7=BA=BF/=E8=A7=A3?= =?UTF-8?q?=E7=A0=81=E5=8F=B3=E8=BD=B4=E5=AE=9E=E7=BA=BF=EF=BC=89+?= =?UTF-8?q?=E7=94=BB=E5=9B=BECSV=E4=B8=80=E9=94=AE=E5=A4=8D=E5=88=B6+?= =?UTF-8?q?=E4=B8=8B=E8=BD=BDPNG+=E9=BB=98=E8=AE=A4=E4=B8=8A=E4=B8=8B?= =?UTF-8?q?=E6=96=87=E9=95=BF=E5=BA=A6=E5=A2=9E=E5=8A=A04096/16384/65536?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API.md | 60 +++++++++++++++++++++++++++----- README.md | 11 +++--- app.py | 83 ++++++++++++++++++++++++++++++++++++++++++++ config.py | 3 ++ static/css/style.css | 12 +++++++ static/js/app.js | 60 ++++++++++++++++++++++++++++++-- tester.py | 2 +- 7 files changed, 216 insertions(+), 15 deletions(-) diff --git a/API.md b/API.md index fb70ed6..15c7aa5 100644 --- a/API.md +++ b/API.md @@ -17,9 +17,10 @@ 3. [连接测试](#3-连接测试) 4. [速度测试](#4-速度测试-tests) 5. [测试详情与日志](#5-测试详情与日志) -6. [导出(Excel / JSON)](#6-导出excel--json) -7. [数据模型](#7-数据模型) -8. [curl 使用示例](#8-curl-使用示例) +6. [折线图(data-chart-tool)](#6-折线图data-chart-tool) +7. [导出(Excel / JSON)](#7-导出excel--json) +8. [数据模型](#8-数据模型) +9. [curl 使用示例](#9-curl-使用示例) --- @@ -113,7 +114,7 @@ }, "gen": { "name": "Qwen3 不同上下文长度速度对比", - "context_lengths": [512, 2048, 8192, 32768, 131072], + "context_lengths": [512, 2048, 4096, 8192, 16384, 32768, 65536, 131072], "max_tokens": 128, "samples": 2, "warmup": true, @@ -127,7 +128,7 @@ | 字段 | 类型 | 默认 | 说明 | |------|------|------|------| | `name` | string | `""` | 测试名称/主题(会存入测试记录并展示在历史与详情) | -| `context_lengths` | number[] | `[512,2048,8192,32768,131072]` | 要测试的上下文长度列表,每个长度独立校准+预热+采样 | +| `context_lengths` | number[] | `[512,2048,4096,8192,16384,32768,65536,131072]` | 要测试的上下文长度列表,每个长度独立校准+预热+采样 | | `max_tokens` | number | `128` | 解码输出 token 长度 | | `samples` | number | `2` | 每个上下文长度的采样次数 | | `warmup` | bool | `true` | 测试前空转预热(不计速度) | @@ -208,7 +209,45 @@ --- -## 6. 导出(Excel / JSON) +## 6. 折线图(data-chart-tool) + +> 调用 [data-chart-tool](http://192.168.2.8:12007/hz4th_coder/data-chart-tool.git) 的 `/api/chart` 接口生成**双Y轴折线图**:左轴=预填充速度(虚线)、右轴=解码速度(实线)、X 轴=上下文长度。本系统在服务端代理转发,前端只需请求本服务的两个接口。 + +### `GET /api/tests//chart` +生成折线图 PNG 图片(内部调用 data-chart-tool `/api/chart`,地址可在 `config.py` 的 `CHART_API_BASE` 修改)。 + +**成功响应:** `Content-Type: image/png`(可直接用于 `` / 下载) + +**失败响应:** +```json +{ "ok": false, "error": "无成功采样数据,无法画图" } // 400 +{ "ok": false, "error": "测试不存在" } // 404 +{ "ok": false, "error": "图表服务不可用: ..." } // 502 +``` + +### `GET /api/tests//chart-data` +返回画图所用的 CSV 数据与图表请求配置,方便手动快速复制。 + +**响应:** +```json +{ + "ok": true, + "csv": "上下文长度, 预填充速度(tok/s), 解码速度(tok/s)\n4096, 126.90, 22.80\n8192, 132.20, 22.40", + "rows": [[4096, 126.9, 22.8], [8192, 132.2, 22.4]], + "payload": { + "data": "...", "chartType": "line", "title": "...", + "dualYAxis": true, + "leftAxisName": "预填充速度(tok/s)", "rightAxisName": "解码速度(tok/s)", + "seriesTypes": ["line", "line"], "seriesAxis": [0, 1], + "seriesStyles": ["dashed", "solid"], "width": 1000, "height": 560, "pixelRatio": 2 + } +} +``` +> `csv` 即画图数据(第一列=上下文长度,第二列=预填充速度,第三列=解码速度),前端「复制画图数据」按钮复制的就是它。 + +--- + +## 7. 导出(Excel / JSON) ### `GET /api/tests//export.xlsx` 导出 Excel 报告(**3 个 Sheet**:汇总 / 采样明细 / 日志),`Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`。 @@ -222,7 +261,7 @@ --- -## 7. 数据模型 +## 8. 数据模型 | 表 | 说明 | 关键字段 | |----|------|----------| @@ -236,7 +275,7 @@ --- -## 8. curl 使用示例 +## 9. curl 使用示例 ```bash BASE=http://:16097 @@ -262,6 +301,11 @@ curl -X POST $BASE/api/tests -H 'Content-Type: application/json' -d '{ curl "$BASE/api/tests?limit=10" curl $BASE/api/tests/9 +# 画图数据(CSV) +curl $BASE/api/tests/9/chart-data +# 折线图 PNG(预填充左轴虚线 / 解码右轴实线) +curl -o chart.png $BASE/api/tests/9/chart + # 导出 curl -OJ $BASE/api/tests/9/export.xlsx curl $BASE/api/tests/9/export.json diff --git a/README.md b/README.md index 8cfb219..da22f80 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ - **访问地址:** `http://:16097/` - **技术栈:** Python 3 + Flask + SQLite(纯 REST,无额外依赖) -- **版本:** v2.0.0 +- **版本:** v2.2.0 --- @@ -17,7 +17,7 @@ ### 🚀 速度测试配置 - **测试名称(主题)**:可为每次测试命名,用于标注测试内容/主题,展示在历史列表、详情弹窗与导出报表中 -- **多上下文长度测试**:默认为 `512 / 2048 / 8192 / 32768 / 131072` tokens,可通过标签点击启用/禁用,并支持手动添加任意自定义长度(≥16) +- **多上下文长度测试**:默认为 `512 / 2048 / 4096 / 8192 / 16384 / 32768 / 65536 / 131072` tokens,可通过标签点击启用/禁用,并支持手动添加任意自定义长度(≥16) - **解码输出长度(max tokens)**:默认为 `128`,可手动自定义 - **每个长度采样次数**:默认为 `2`,可手动自定义 - **测试前预热(空转)**:默认开启,先发一次不计速度的空转请求,避免冷启动/首请求偏慢污染真实采样数据 @@ -31,6 +31,7 @@ - 实时控制台日志:校准、预热、每次采样明细全程可追溯 - **每次完整测试**支持: - **网页点击查看**:历史记录「查看」按钮弹出详情(整体统计 + 按上下文长度分组 + 每次采样明细 + 完整日志) + - **📈 折线图(data-chart-tool)**:详情弹窗内点击「生成/刷新折线图」,调用 data-chart-tool 的 `/api/chart` 画**双Y轴折线图**——左轴=预填充速度(虚线),右轴=解码速度(实线),X 轴为上下文长度;并提供**画图 CSV 数据一键复制 / 下载 PNG** - **文件下载 Excel(xlsx)**:历史记录「Excel」按钮或详情弹窗「导出 Excel」,包含 汇总 / 采样明细 / 日志 三个 Sheet - **文件下载 JSON**:详情弹窗「导出 JSON」 - 测试历史留存(含测试名称),可随时刷新、查看、导出、删除 @@ -97,6 +98,8 @@ pip install -r requirements.txt | DELETE | `/api/tests/` | 删除测试 | | GET | `/api/tests//export.xlsx` | 导出 Excel 报告(汇总/采样明细/日志三 Sheet) | | GET | `/api/tests//export.json` | 导出完整测试 JSON | +| GET | `/api/tests//chart` | 用 data-chart-tool 生成折线图 PNG(预填充左轴虚线 / 解码右轴实线) | +| GET | `/api/tests//chart-data` | 画图数据(CSV + 图表请求配置,供一键复制) | > 完整字段说明、响应示例与 curl 示例见 **API.md**。 @@ -113,7 +116,7 @@ POST /api/tests "temperature": 0.7 }, "gen": { - "context_lengths": [512, 2048, 8192, 32768, 131072], + "context_lengths": [512, 2048, 4096, 8192, 16384, 32768, 65536, 131072], "max_tokens": 128, "samples": 2, "warmup": true, @@ -161,4 +164,4 @@ llm-speed-tester/ ## Git - **仓库:** `hz4th_coder/llm-speed-tester` -- **版本:** v2.1.0(多上下文长度测试 + 预热 + Excel/JSON 导出 + 测试名称 + 整体统计平均/最小/最大 + 开放 API + 推理型模型兼容) +- **版本:** v2.2.0(多上下文长度测试 + 预热 + Excel/JSON 导出 + 测试名称 + 整体统计平均/最小/最大 + 开放 API + 推理型模型兼容 + 📈 data-chart-tool 双Y轴折线图/画图数据复制 + 默认上下文长度增加 4096/16384/65536) diff --git a/app.py b/app.py index 21e7ec4..2d86ec5 100644 --- a/app.py +++ b/app.py @@ -3,6 +3,7 @@ import io import json +import requests from flask import Flask, jsonify, request, send_file, send_from_directory import config @@ -160,6 +161,88 @@ def del_test(tid): return jsonify({"ok": True}) +# ───────────────────────── 图表(data-chart-tool 折线图:预填充左轴虚线 / 解码右轴实线) ───────────────────────── + +def _build_chart_csv(t): + """由测试汇总 by_length 构建画图 CSV:上下文长度, 预填充速度(tok/s), 解码速度(tok/s)""" + s = t.get("summary") or {} + by = s.get("by_length") or {} + lens = sorted(int(k) for k in by) + rows = [] + for L in lens: + bl = by.get(str(L)) if str(L) in by else by.get(L) or {} + pre = bl.get("avg_prefill_speed") + dec = bl.get("avg_decode_speed") + if pre is None or dec is None: + continue + rows.append([L, round(pre, 2), round(dec, 2)]) + if not rows: + return None + csv_lines = ["上下文长度, 预填充速度(tok/s), 解码速度(tok/s)"] + for L, pre, dec in rows: + csv_lines.append("%d, %.2f, %.2f" % (L, pre, dec)) + return {"csv": "\n".join(csv_lines), "rows": rows} + + +def _chart_payload(t, csv_text): + """组装 data-chart-tool /api/chart 请求体(双Y轴折线图)""" + title = ("%s %s" % (t.get("model") or "", t.get("name") or "速度对比")).strip() + return { + "data": csv_text, + "chartType": "line", + "title": title, + "theme": "default", + "showLegend": True, + "showGrid": True, + "showLabel": False, + "smoothLine": True, + "dualYAxis": True, + "leftAxisName": "预填充速度(tok/s)", + "rightAxisName": "解码速度(tok/s)", + "seriesTypes": ["line", "line"], + "seriesAxis": [0, 1], + "seriesStyles": ["dashed", "solid"], # 预填充=左轴虚线,解码=右轴实线 + "width": 1000, + "height": 560, + "pixelRatio": 2, + } + + +@app.route("/api/tests//chart-data") +def test_chart_data(tid): + t = db.get_test(tid) + if not t: + return jsonify({"ok": False, "error": "测试不存在"}), 404 + built = _build_chart_csv(t) + if not built: + return jsonify({"ok": False, "error": "无成功采样数据,无法画图"}), 400 + return jsonify({ + "ok": True, + "csv": built["csv"], + "rows": built["rows"], + "payload": _chart_payload(t, built["csv"]), + }) + + +@app.route("/api/tests//chart") +def test_chart(tid): + """用 data-chart-tool 生成折线图 PNG(预填充左轴虚线 / 解码右轴实线)""" + t = db.get_test(tid) + if not t: + return jsonify({"ok": False, "error": "测试不存在"}), 404 + built = _build_chart_csv(t) + if not built: + return jsonify({"ok": False, "error": "无成功采样数据,无法画图"}), 400 + try: + resp = requests.post(config.CHART_API_BASE + "/api/chart", + json=_chart_payload(t, built["csv"]), timeout=60) + except requests.RequestException as e: + return jsonify({"ok": False, "error": "图表服务不可用: %s" % e}), 502 + if resp.status_code != 200: + return jsonify({"ok": False, "error": "图表生成失败(%d): %s" % (resp.status_code, resp.text[:300])}), 502 + return send_file(io.BytesIO(resp.content), mimetype="image/png") + + @app.route("/api/tests//export.json") def export_json(tid): t = db.get_test(tid) diff --git a/config.py b/config.py index e801e33..273d21c 100644 --- a/config.py +++ b/config.py @@ -13,3 +13,6 @@ DB_PATH = os.path.join(DATA_DIR, "llm_speed_tester.db") # 流式请求超时:连接 60s,两次数据包间隔最长 300s(推理型/长上文模型也够用) CONNECT_TIMEOUT = 60 STREAM_READ_TIMEOUT = 300 + +# data-chart-tool 图表服务地址(用它的 /api/chart 画折线图:预填充左轴虚线 / 解码右轴实线) +CHART_API_BASE = "http://127.0.0.1:16016" diff --git a/static/css/style.css b/static/css/style.css index 9bd8f4d..a8240f9 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -206,6 +206,18 @@ table.mini { width: 100%; border-collapse: collapse; font-size: 12.5px; } .detail-log .ln.ERROR { color: var(--danger); } .detail-log .ln.WARN { color: var(--warn); } +/* ── 图表(data-chart-tool 折线图) ── */ +.chart-block { background: var(--panel2); border: 1px solid var(--border); border-radius: 10px; padding: 12px; } +.chart-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; } +.chart-img-wrap { margin-top: 10px; min-height: 60px; display: flex; align-items: center; justify-content: center; border: 1px dashed var(--border); border-radius: 8px; padding: 10px; background: #0a0e17; } +.chart-img-wrap img { max-width: 100%; border-radius: 6px; } +.chart-data-head { margin-top: 10px; font-size: 12px; color: var(--muted); } +.chart-csv { + width: 100%; min-height: 120px; margin-top: 6px; background: #0a0e17; + border: 1px solid var(--border); border-radius: 8px; color: #a8f0d0; + font: 12px/1.7 var(--mono); padding: 10px; resize: vertical; outline: none; +} + pre.json-box { background: #0a0e17; border: 1px solid var(--border); border-radius: 8px; padding: 10px; font: 11.5px/1.6 var(--mono); overflow: auto; max-height: 260px; color: #a8f0d0; diff --git a/static/js/app.js b/static/js/app.js index 458e3a1..b964068 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -21,8 +21,8 @@ let pollTimer = null; let lastLogId = 0; let consoleLogs = []; // 当前测试已加载日志 [{id,level,msg,rel}] -// 上下文长度:chips 列表 + 启用集合(默认 512/2048/8192/32768/131072) -const DEFAULT_CONTEXT_LENGTHS = [512, 2048, 8192, 32768, 131072]; +// 上下文长度:chips 列表 + 启用集合(默认 512/2048/4096/8192/16384/32768/65536/131072) +const DEFAULT_CONTEXT_LENGTHS = [512, 2048, 4096, 8192, 16384, 32768, 65536, 131072]; let contextLengths = [...DEFAULT_CONTEXT_LENGTHS]; let contextLengthsActive = new Set(contextLengths); @@ -421,6 +421,23 @@ async function viewDetail(id) {

📏 按上下文长度汇总

${byLengthHtml} +

📈 折线图(预填充左轴虚线 / 解码右轴实线)

+
+
+ + +
+
+
点击上方按钮,用 data-chart-tool 生成双Y轴折线图(左轴=预填充虚线,右轴=解码实线)
+
+
📋 画图数据(CSV,可一键复制)
+ +
+ + +
+
+

⚙️ 测试参数

测试名称
${esc(t.name || "—")}
@@ -444,6 +461,45 @@ async function viewDetail(id) { $("#detail-mask").hidden = false; $("#dt-body").scrollTop = 0; window.__detail = t; + + // 图表:生成/刷新 + 复制数据 + 下载 + const genBtn = $("#dt-chart-gen"); + const csvTa = $("#dt-chart-csv"); + const imgWrap = $("#dt-chart-img"); + const statusEl = $("#dt-chart-status"); + let chartUrl = ""; + const genChart = () => { + statusEl.textContent = "⏳ 正在生成..."; + fetch(`/api/tests/${id}/chart-data`).then((r) => r.json()).then((d) => { + if (!d.ok) { statusEl.textContent = "❌ " + (d.error || "生成失败"); return; } + csvTa.value = d.csv; + chartUrl = `/api/tests/${id}/chart?t=${Date.now()}`; + const img = document.createElement("img"); + img.src = chartUrl; + img.alt = "预填充/解码 折线图"; + img.style.maxWidth = "100%"; + img.onload = () => { imgWrap.innerHTML = ""; imgWrap.appendChild(img); statusEl.textContent = "✅ 生成完成"; }; + img.onerror = () => { statusEl.textContent = "❌ 图表生成失败"; }; + }).catch((e) => { statusEl.textContent = "❌ " + e.message; }); + }; + genBtn.addEventListener("click", genChart); + $("#dt-chart-copy").addEventListener("click", () => { + if (!csvTa.value) { toast("暂无画图数据,请先生成图表"); return; } + csvTa.select(); + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(csvTa.value).then(() => toast("画图数据已复制")); + } else { + document.execCommand("copy"); + toast("画图数据已复制"); + } + }); + $("#dt-chart-download").addEventListener("click", () => { + if (!chartUrl) { toast("请先生成图表"); return; } + const a = document.createElement("a"); + a.href = chartUrl; + a.download = `llm_speed_chart_${id}.png`; + document.body.appendChild(a); a.click(); a.remove(); + }); } function closeDetail() { $("#detail-mask").hidden = true; } diff --git a/tester.py b/tester.py index b87c5a7..264e508 100644 --- a/tester.py +++ b/tester.py @@ -51,7 +51,7 @@ class TestRunner(threading.Thread): model = self.cfg.get("model", "") gen = self.gen - # 上下文长度列表(支持手动自定义,默认 512/2048/8192/32768/131072) + # 上下文长度列表(支持手动自定义,默认 512/2048/4096/8192/16384/32768/65536/131072) raw_lengths = gen.get("context_lengths") or [] if not raw_lengths: # 兼容旧版单值配置