v2.0.0:多上下文长度测试(默认512/2048/8192/32768/131072)+测试前空转预热(不计速度)+解码输出默认128+每长度采样默认2+Excel(xlsx)导出+详情按长度分组+接口输入框图标美化+README文档

This commit is contained in:
2026-08-23 18:30:17 +08:00
parent 17f32385a6
commit 87b836b52a
8 changed files with 555 additions and 63 deletions
+112 -7
View File
@@ -21,6 +21,11 @@ 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];
let contextLengths = [...DEFAULT_CONTEXT_LENGTHS];
let contextLengthsActive = new Set(contextLengths);
const esc = (s) => String(s ?? "").replace(/[&<>"']/g,
(c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
const fmt = (v, d = "—") => (v === null || v === undefined || isNaN(v) ? d : v);
@@ -50,14 +55,58 @@ function currentConfig() {
}
function currentGen() {
const lens = contextLengths.filter((l) => contextLengthsActive.has(l));
return {
prompt_tokens: parseInt($("#gen-prompt-tokens").value) || 2048,
max_tokens: parseInt($("#gen-max-tokens").value) || 256,
samples: parseInt($("#gen-samples").value) || 3,
context_lengths: lens.length ? lens : [2048],
max_tokens: parseInt($("#gen-max-tokens").value) || 128,
samples: parseInt($("#gen-samples").value) || 2,
avoid_cache: $("#gen-avoid-cache").checked,
warmup: $("#gen-warmup").checked,
};
}
/* ───────────────────────── 上下文长度 chips ───────────────────────── */
function renderChips() {
const box = $("#gen-contexts");
box.innerHTML = "";
if (!contextLengths.length) {
box.innerHTML = '<span class="chips-empty">暂无长度,点击下方“添加”自定义</span>';
return;
}
for (const len of contextLengths) {
const active = contextLengthsActive.has(len);
const c = document.createElement("span");
c.className = "chip" + (active ? "" : " off");
c.title = "点击启用/禁用";
c.innerHTML = `<span class="chip-v">${len}</span><span class="chip-x">✕</span>`;
c.addEventListener("click", (e) => {
if (e.target.closest(".chip-x")) {
contextLengths = contextLengths.filter((x) => x !== len);
contextLengthsActive.delete(len);
renderChips();
} else {
if (contextLengthsActive.has(len)) contextLengthsActive.delete(len);
else contextLengthsActive.add(len);
renderChips();
}
});
box.appendChild(c);
}
}
function addContextLength() {
const v = parseInt($("#gen-context-add").value);
if (!v || v < 16) { toast("请输入有效长度(≥16"); return; }
if (!contextLengths.includes(v)) {
contextLengths.push(v);
contextLengthsActive.add(v);
contextLengths.sort((a, b) => a - b);
renderChips();
}
$("#gen-context-add").value = "";
}
function updateDefaultUrlHint() {
const p = $("#cfg-provider").value;
$("#cfg-default-url").textContent = "默认地址:" + PROVIDER_DEFAULT_URL[p];
@@ -269,6 +318,7 @@ async function loadHistory() {
<td><span class="status-pill ${esc(t.status)}">${STATUS_LABEL[t.status] || t.status}</span></td>
<td>
<button class="btn small" data-view="${t.id}">查看</button>
<button class="btn small" data-xlsx="${t.id}">Excel</button>
<button class="btn small danger" data-del="${t.id}">删除</button>
</td>`;
tb.appendChild(tr);
@@ -284,17 +334,43 @@ async function viewDetail(id) {
const cfg = t.config || {};
const runs = t.runs || [];
const logs = t.logs || [];
const byLength = s.by_length || {};
let byLengthHtml;
const lens = Object.keys(byLength).sort((a, b) => a - b);
if (lens.length) {
byLengthHtml = `<table class="mini"><thead><tr>
<th>上下文长度tok</th><th>采样(成功/总数)</th><th>首字ms</th><th>预填充tok/s</th>
<th>解码tok/s</th><th>提示词tok</th><th>输出tok</th><th>总耗时ms</th>
</tr></thead><tbody>` +
lens.map((L) => {
const bl = byLength[L] || {};
return `<tr>
<td class="num">${L}</td>
<td class="num">${fmt(bl.samples_ok)}/${fmt(bl.samples_total)}</td>
<td class="num">${fmt(bl.avg_ttft_ms)}</td>
<td class="num">${fmt(bl.avg_prefill_speed)}</td>
<td class="num">${fmt(bl.avg_decode_speed)}</td>
<td class="num">${fmt(bl.avg_prompt_tokens)}</td>
<td class="num">${fmt(bl.avg_output_tokens)}</td>
<td class="num">${fmt(bl.avg_total_ms)}</td>
</tr>`;
}).join("") + `</tbody></table>`;
} else {
byLengthHtml = '<div class="hint">无成功采样数据</div>';
}
let runsHtml;
if (runs.length) {
runsHtml = `<table class="mini"><thead><tr>
<th>采样</th><th>提示词tok</th><th>缓存tok</th><th>首字ms</th><th>预填充tok/s</th>
<th>采样</th><th>上下文tok</th><th>提示词tok</th><th>缓存tok</th><th>首字ms</th><th>预填充tok/s</th>
<th>输出tok</th><th>解码tok/s</th><th>总耗时ms</th><th>备注</th>
</tr></thead><tbody>` +
runs.map((r, i) => {
const m = r.metrics || {};
return `<tr class="${r.error ? "err" : ""}">
<td>${i + 1}</td>
<td class="num">${r.context_length || m.context_length || "—"}</td>
<td class="num">${fmt(m.prompt_tokens)}</td>
<td class="num">${fmt(m.cached_tokens, 0)}</td>
<td class="num">${fmt(m.ttft_ms)}</td>
@@ -331,11 +407,15 @@ async function viewDetail(id) {
<div class="kv-item"><div class="kv-k">平均总耗时</div><div class="kv-v">${fmt(s.avg_total_ms)} ms</div></div>
</div>
<h3>📏 按上下文长度汇总</h3>
${byLengthHtml}
<h3>⚙️ 测试参数</h3>
<div class="kv">
<div class="kv-item"><div class="kv-k">上文长度</div><div class="kv-v">${g.prompt_tokens ?? "—"} tok</div></div>
<div class="kv-item"><div class="kv-k">生成长度</div><div class="kv-v">${g.max_tokens ?? "—"} tok</div></div>
<div class="kv-item"><div class="kv-k">采样次数</div><div class="kv-v">${g.samples ?? "—"}</div></div>
<div class="kv-item"><div class="kv-k">上文长度</div><div class="kv-v">${(g.context_lengths || []).join(" / ") || "—"} tok</div></div>
<div class="kv-item"><div class="kv-k">解码输出长度</div><div class="kv-v">${g.max_tokens ?? "—"} tok</div></div>
<div class="kv-item"><div class="kv-k">每个长度采样</div><div class="kv-v">${g.samples ?? "—"}</div></div>
<div class="kv-item"><div class="kv-k">预热(空转)</div><div class="kv-v">${g.warmup === false ? "关" : "开"}</div></div>
<div class="kv-item"><div class="kv-k">避免缓存</div><div class="kv-v">${g.avoid_cache ? "开" : "关"}</div></div>
<div class="kv-item"><div class="kv-k">温度</div><div class="kv-v">${fmt(cfg.temperature)}</div></div>
<div class="kv-item"><div class="kv-k">Base URL</div><div class="kv-v">${esc(cfg.base_url || "(默认)")}</div></div>
@@ -362,6 +442,25 @@ function exportDetail() {
download(JSON.stringify(window.__detail, null, 2), `test_${window.__detail.id}.json`, "application/json");
}
async function exportXlsx(id) {
try {
const resp = await fetch(`/api/tests/${id}/export.xlsx`);
if (!resp.ok) {
const j = await resp.json().catch(() => ({}));
throw new Error(j.error || "导出失败");
}
const blob = await resp.blob();
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = `llm_speed_test_${id}.xlsx`;
document.body.appendChild(a);
a.click();
setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 100);
} catch (e) {
toast("导出失败:" + e.message);
}
}
/* ───────────────────────── 导出 / 下载 ───────────────────────── */
function download(text, filename, type = "text/plain") {
@@ -424,6 +523,8 @@ function bind() {
$("#btn-test-conn").addEventListener("click", testConnection);
$("#btn-start").addEventListener("click", startTest);
$("#btn-cancel").addEventListener("click", stopTest);
$("#btn-context-add").addEventListener("click", addContextLength);
$("#gen-context-add").addEventListener("keydown", (e) => { if (e.key === "Enter") addContextLength(); });
$("#btn-clear-console").addEventListener("click", clearConsole);
$("#btn-export-log").addEventListener("click", exportCurrentLog);
@@ -431,8 +532,10 @@ function bind() {
$("#history tbody").addEventListener("click", (e) => {
const v = e.target.closest("[data-view]");
const x = e.target.closest("[data-xlsx]");
const d = e.target.closest("[data-del]");
if (v) viewDetail(Number(v.dataset.view));
if (x) exportXlsx(Number(x.dataset.xlsx));
if (d) {
const id = Number(d.dataset.del);
if (confirm(`确定删除测试 #${id} 及其全部日志?`)) {
@@ -444,6 +547,7 @@ function bind() {
$("#dt-close").addEventListener("click", closeDetail);
$("#detail-mask").addEventListener("click", (e) => { if (e.target === $("#detail-mask")) closeDetail(); });
$("#dt-export").addEventListener("click", exportDetail);
$("#dt-export-xlsx").addEventListener("click", () => { if (window.__detail) exportXlsx(window.__detail.id); });
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeDetail(); });
}
@@ -452,6 +556,7 @@ function bind() {
(async function init() {
bind();
renderChips();
updateDefaultUrlHint();
loadConfigs();
loadHistory();