v2.0.0:多上下文长度测试(默认512/2048/8192/32768/131072)+测试前空转预热(不计速度)+解码输出默认128+每长度采样默认2+Excel(xlsx)导出+详情按长度分组+接口输入框图标美化+README文档
This commit is contained in:
+112
-7
@@ -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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[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();
|
||||
|
||||
Reference in New Issue
Block a user