Files
vllm-omni/tools/nightly/generate_nightly_perf_html.py
T
+1 f92d84fcf9 [Rebase] Rebase to vllm releases/v0.22.0 (#3891)
Signed-off-by: tzhouam <tzhouam@connect.ust.hk>
Signed-off-by: ZhengWG <zwg0606@gmail.com>
Signed-off-by: david6666666 <530634352@qq.com>
Signed-off-by: Yueqian Lin <linyueqian@outlook.com>
Signed-off-by: nainiu258 <cperfect02@163.com>
Signed-off-by: nainiu258 <101917677+nainiu258@users.noreply.github.com>
Signed-off-by: JasonJ2021 <jasonj@zju.edu.cn>
Signed-off-by: Zhou Taichang <tzhouam@connect.ust.hk>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: skf <54565339+skf-1999@users.noreply.github.com>
Co-authored-by: Zheng Wengang <zwg0606@gmail.com>
Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
Co-authored-by: WeiQing Chen <40507679+david6666666@users.noreply.github.com>
Co-authored-by: SYLAR <125541396+lishunyang12@users.noreply.github.com>
Co-authored-by: Yueqian Lin <linyueqian@outlook.com>
Co-authored-by: nainiu258 <101917677+nainiu258@users.noreply.github.com>
Co-authored-by: Jason <72191212+JasonJ2021@users.noreply.github.com>
Co-authored-by: Yueqian Lin <70319226+linyueqian@users.noreply.github.com>
2026-06-01 11:08:12 +08:00

1863 lines
61 KiB
Python

#!/usr/bin/env python3
"""
Generate an improved nightly HTML performance dashboard without modifying the
existing generator.
"""
from __future__ import annotations
import argparse
import json
import logging
import os
from collections.abc import Iterable, Sequence
from datetime import datetime, timezone
from typing import Any
from benchmarks.diffusion.backends import normalize_endpoint
LOGGER = logging.getLogger(__name__)
_RESULT_JSON_PREFIX = "result_test_"
_DIFFUSION_JSON_PREFIXES = ("diffusion_perf_", "diffusion_result_")
DEFAULT_INPUT_DIR = os.getenv("DEFAULT_INPUT_DIR") or "tests"
DEFAULT_OUTPUT_DIR = os.getenv("DEFAULT_OUTPUT_DIR") or "tests"
DEFAULT_DIFFUSION_INPUT_DIR = os.getenv("DIFFUSION_BENCHMARK_DIR")
def _vllm_omni_root() -> str:
path = os.path.dirname(os.path.abspath(__file__))
while path and path != os.path.dirname(path):
if os.path.isdir(os.path.join(path, "tests")):
return path
path = os.path.dirname(path)
return os.path.normpath(
os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."),
)
def _default_input_dir() -> str:
return os.path.join(_vllm_omni_root(), DEFAULT_INPUT_DIR)
def _default_output_file() -> str:
ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
return os.path.join(
_vllm_omni_root(),
DEFAULT_OUTPUT_DIR,
f"nightly_perf_{ts}.html",
)
def _default_diffusion_input_dir(input_dir: str) -> str:
return DEFAULT_DIFFUSION_INPUT_DIR if DEFAULT_DIFFUSION_INPUT_DIR else input_dir
def _load_json_file(path: str) -> dict[str, Any] | list[Any] | None:
try:
with open(path, encoding="utf-8") as f:
data = json.load(f)
except (OSError, json.JSONDecodeError) as exc:
LOGGER.warning("failed to load json '%s': %s", path, exc)
return None
if not isinstance(data, (dict, list)):
LOGGER.warning("json root in '%s' is not an object or list, skip", path)
return None
return data
def _parse_from_filename(filename: str) -> dict[str, Any]:
"""Parse ``result_test_*.json`` filenames; same rules as ``generate_nightly_perf_excel``."""
name, ext = os.path.splitext(filename)
if ext != ".json" or not name.startswith(_RESULT_JSON_PREFIX):
return {}
core = name[len(_RESULT_JSON_PREFIX) :]
parts = core.split("_")
if len(parts) < 5:
LOGGER.warning(
"filename '%s' does not match expected pattern (need >= 5 segments), skip parsing",
filename,
)
return {}
idx = len(parts) - 1
timestamp = parts[idx]
idx -= 1
parsed: dict[str, Any] = {}
if len(timestamp) >= 15:
parsed["date"] = timestamp
if idx >= 0 and parts[idx].startswith("out"):
parsed["random_output_len"] = parts[idx][3:]
idx -= 1
if idx >= 0 and parts[idx].startswith("in"):
parsed["random_input_len"] = parts[idx][2:]
idx -= 1
if idx < 3:
LOGGER.warning(
"filename '%s' has too few segments after timestamp / optional in-out (idx=%s)",
filename,
idx,
)
return parsed
num_prompts_str = parts[idx]
idx -= 1
flow_str = parts[idx]
idx -= 1
dataset_name = parts[idx]
idx -= 1
test_name = "_".join(parts[: idx + 1]) if idx >= 0 else ""
try:
parsed["num_prompts"] = int(num_prompts_str)
except (TypeError, ValueError):
pass
try:
parsed["max_concurrency"] = int(flow_str)
except (TypeError, ValueError):
pass
if test_name:
parsed["test_name"] = test_name
if dataset_name in ("random", "random-mm"):
parsed["dataset_name"] = dataset_name
return parsed
def _iter_omni_json_records(input_dir: str) -> Iterable[dict[str, Any]]:
if not os.path.isdir(input_dir):
LOGGER.warning("input dir '%s' does not exist or is not a directory", input_dir)
return
for entry in sorted(os.listdir(input_dir)):
if not entry.endswith(".json") or not entry.startswith(_RESULT_JSON_PREFIX):
continue
full_path = os.path.join(input_dir, entry)
if not os.path.isfile(full_path):
continue
data = _load_json_file(full_path)
if data is None:
continue
record: dict[str, Any] = dict(data)
filename_meta = _parse_from_filename(os.path.basename(full_path))
if "date" not in record or not record["date"]:
record["date"] = filename_meta.get("date") or datetime.now(
timezone.utc,
).strftime("%Y%m%d-%H%M%S")
if "num_prompts" not in record or record["num_prompts"] is None:
if "num_prompts" in filename_meta:
record["num_prompts"] = filename_meta["num_prompts"]
if "max_concurrency" not in record or record["max_concurrency"] is None:
if "max_concurrency" in filename_meta:
record["max_concurrency"] = filename_meta["max_concurrency"]
if "test_name" not in record or not record.get("test_name"):
if "test_name" in filename_meta:
record["test_name"] = filename_meta["test_name"]
if "dataset_name" not in record or not record.get("dataset_name"):
if "dataset_name" in filename_meta:
record["dataset_name"] = filename_meta["dataset_name"]
record["source_file"] = os.path.basename(full_path)
yield record
def _parse_diffusion_from_filename(filename: str) -> dict[str, Any]:
name, ext = os.path.splitext(filename)
if ext != ".json" or not any(name.startswith(prefix) for prefix in _DIFFUSION_JSON_PREFIXES):
return {}
matched_prefix = next(prefix for prefix in _DIFFUSION_JSON_PREFIXES if name.startswith(prefix))
core = name[len(matched_prefix) :]
parts = core.split("_")
if len(parts) < 2:
return {}
timestamp = parts[-1]
test_name = "_".join(parts[:-1]) if parts[:-1] else ""
parsed: dict[str, Any] = {}
if len(timestamp) >= 15:
parsed["date"] = timestamp
if test_name:
parsed["test_name"] = test_name
return parsed
def _iter_diffusion_json_records(input_dir: str) -> Iterable[dict[str, Any]]:
if not os.path.isdir(input_dir):
LOGGER.warning(
"diffusion input dir '%s' does not exist or is not a directory",
input_dir,
)
return
for entry in sorted(os.listdir(input_dir)):
if not entry.endswith(".json") or not any(entry.startswith(prefix) for prefix in _DIFFUSION_JSON_PREFIXES):
continue
full_path = os.path.join(input_dir, entry)
if not os.path.isfile(full_path):
continue
data = _load_json_file(full_path)
if data is None:
continue
filename_meta = _parse_diffusion_from_filename(os.path.basename(full_path))
if isinstance(data, dict):
records = [data]
elif isinstance(data, list):
records = [r for r in data if isinstance(r, dict)]
else:
records = []
if not records:
LOGGER.warning("diffusion json '%s' has no valid records, skip", full_path)
continue
for record in records:
flat: dict[str, Any] = dict(record)
result = flat.pop("result", None)
if isinstance(result, dict):
flat.update(result)
if flat.get("endpoint"):
flat["endpoint"] = normalize_endpoint(flat["endpoint"])
elif flat.get("backend"):
flat["endpoint"] = normalize_endpoint(flat["backend"])
flat.pop("backend", None)
flat.pop("API Backend", None)
if "date" not in flat or not flat.get("date"):
flat["date"] = filename_meta.get("date") or datetime.now(
timezone.utc,
).strftime("%Y%m%d-%H%M%S")
if "test_name" not in flat or not flat.get("test_name"):
if "test_name" in filename_meta:
flat["test_name"] = filename_meta["test_name"]
flat["source_file"] = os.path.basename(full_path)
yield flat
def _collect_omni_records(input_dir: str) -> list[dict[str, Any]]:
return list(_iter_omni_json_records(input_dir))
def _collect_diffusion_records(input_dir: str) -> list[dict[str, Any]]:
return list(_iter_diffusion_json_records(input_dir))
def _sort_omni_records(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
by_date_desc = sorted(records, key=lambda r: r.get("date") or "", reverse=True)
return sorted(
by_date_desc,
key=lambda r: (
r.get("model_id") or "",
r.get("test_name") or "",
r.get("dataset_name") or "",
r.get("max_concurrency") or 0,
r.get("num_prompts") or 0,
),
)
def _sort_diffusion_records(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
by_date_desc = sorted(records, key=lambda r: r.get("date") or "", reverse=True)
return sorted(by_date_desc, key=lambda r: r.get("test_name") or "")
def _ensure_parent_dir(path: str) -> None:
parent = os.path.dirname(os.path.abspath(path))
if parent:
os.makedirs(parent, exist_ok=True)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate an improved nightly HTML dashboard from performance JSON files."
)
parser.add_argument(
"--input-dir",
type=str,
default=_default_input_dir(),
help="Directory containing result_test_*.json files.",
)
parser.add_argument(
"--output-file",
type=str,
default=_default_output_file().replace(".html", "_v2.html"),
help="Output path of the HTML report.",
)
parser.add_argument(
"--diffusion-input-dir",
type=str,
default=None,
help="Directory containing diffusion_perf_*.json files; default falls back to --input-dir.",
)
return parser.parse_args()
def _build_html_document(
omni_columns: Sequence[str],
omni_records: Sequence[dict[str, object]],
diffusion_columns: Sequence[str],
diffusion_records: Sequence[dict[str, object]],
) -> str:
styles = """
:root {
--bg: #0b1018;
--surface: rgba(26, 35, 50, 0.9);
--surface-2: rgba(17, 24, 39, 0.96);
--border: #2d3a4f;
--text: #e6edf3;
--muted: #8b949e;
--accent: #58a6ff;
--accent-2: #3fb950;
--accent-3: #f0883e;
--shadow: 0 18px 45px rgba(0, 0, 0, 0.28);
}}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans SC", sans-serif;
background:
radial-gradient(circle at top left, rgba(88, 166, 255, 0.18), transparent 26%),
radial-gradient(circle at top right, rgba(63, 185, 80, 0.12), transparent 22%),
linear-gradient(180deg, #111827, #0b1018 64%);
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
opacity: 0.12;
background:
linear-gradient(rgba(255,255,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.03) 1px, transparent 1px);
background-size: 24px 24px;
}
.container { max-width: 1500px; margin: 0 auto; padding: 24px 20px 64px; }
.hero, section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 22px;
box-shadow: var(--shadow);
backdrop-filter: blur(8px);
}
.hero {
padding: 28px 30px;
margin-bottom: 22px;
background: linear-gradient(145deg, rgba(17, 24, 39, 0.96), rgba(26, 35, 50, 0.88));
}
h1 { margin: 0 0 8px 0; font-size: 2rem; line-height: 1.08; }
.meta { color: var(--muted); font-size: 0.96rem; max-width: 820px; }
.hero-badges, .section-pills {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin-top: 16px;
}
.badge, .pill {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 0.4rem 0.74rem;
border-radius: 999px;
border: 1px solid var(--border);
background: rgba(255,255,255,0.04);
color: var(--muted);
font-size: 0.84rem;
}
.badge strong, .pill strong { color: var(--text); }
section { padding: 18px; margin-bottom: 22px; }
.section-head {
display: flex;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
align-items: flex-start;
margin-bottom: 14px;
}
.section-title { margin: 0; font-size: 1.24rem; }
.section-subtitle { color: var(--muted); font-size: 0.9rem; margin-top: 4px; }
.stats-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.stat-card {
background: rgba(255,255,255,0.03);
border: 1px solid var(--border);
border-radius: 16px;
padding: 14px;
}
.stat-label {
color: var(--muted);
font-size: 0.78rem;
letter-spacing: 0.06em;
text-transform: uppercase;
margin-bottom: 8px;
}
.stat-value { font-size: 1.3rem; font-weight: 700; line-height: 1.12; }
.stat-sub { color: var(--muted); font-size: 0.82rem; margin-top: 6px; }
.toolbar {
display: grid;
gap: 12px;
margin-bottom: 12px;
}
.toolbar-row {
display: grid;
gap: 12px;
min-width: 0;
}
.toolbar-row.primary {
grid-template-columns: minmax(0, 2fr) repeat(3, minmax(0, 1fr));
}
.toolbar-row.secondary {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.filter-field { display: flex; flex-direction: column; gap: 6px; }
.filter-field label { color: var(--muted); font-size: 0.82rem; padding-left: 2px; }
.filter-field.compact label { font-size: 0.78rem; }
.filter-field { min-width: 0; }
.filter-input, .ghost {
width: 100%;
min-width: 0;
border-radius: 12px;
border: 1px solid var(--border);
padding: 0.72rem 0.88rem;
background: rgba(255,255,255,0.035);
color: var(--text);
outline: none;
}
.filter-input {
overflow: hidden;
text-overflow: ellipsis;
}
.filter-select {
width: 100%;
min-width: 0;
border-radius: 12px;
border: 1px solid var(--border);
padding: 0.72rem 2.1rem 0.72rem 0.88rem;
background: rgba(255,255,255,0.035);
color: var(--text);
outline: none;
appearance: none;
-webkit-appearance: none;
background-image:
linear-gradient(45deg, transparent 50%, var(--muted) 50%),
linear-gradient(135deg, var(--muted) 50%, transparent 50%);
background-position:
calc(100% - 18px) calc(50% - 2px),
calc(100% - 12px) calc(50% - 2px);
background-size: 6px 6px, 6px 6px;
background-repeat: no-repeat;
}
.filter-select:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.16);
}
.filter-input:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(88, 166, 255, 0.16);
}
.ghost {
cursor: pointer;
background: rgba(88, 166, 255, 0.08);
}
.ghost:hover { background: rgba(88, 166, 255, 0.16); }
.hint { color: var(--muted); font-size: 0.84rem; }
.layout {
display: grid;
grid-template-columns: minmax(0, 1.7fr) minmax(280px, 0.9fr);
gap: 16px;
align-items: start;
margin: 14px 0 16px;
}
.charts-stack { display: grid; gap: 14px; }
.chart-card, .panel, .table-shell {
background: var(--surface-2);
border: 1px solid var(--border);
border-radius: 18px;
}
.chart-card { padding: 14px; }
.chart-card.empty {
min-height: 180px;
display: flex;
align-items: center;
justify-content: center;
color: var(--muted);
}
.chart-head {
display: flex;
justify-content: space-between;
gap: 12px;
margin-bottom: 8px;
}
.chart-title { font-size: 0.96rem; font-weight: 600; }
.chart-caption { color: var(--muted); font-size: 0.8rem; }
.chart-wrap { position: relative; }
canvas { width: 100%; height: 320px; display: block; }
.chart-tooltip {
position: absolute;
pointer-events: none;
display: none;
max-width: 340px;
padding: 0.55rem 0.7rem;
border-radius: 12px;
border: 1px solid var(--border);
background: rgba(11, 16, 24, 0.97);
box-shadow: 0 12px 30px rgba(0,0,0,0.4);
font-size: 0.84rem;
}
.panel {
position: sticky;
top: 16px;
padding: 14px;
}
.panel h3 { margin: 0 0 12px 0; font-size: 1rem; }
.kv { display: grid; gap: 10px; }
.kv-row {
display: flex;
justify-content: space-between;
gap: 10px;
border-bottom: 1px solid rgba(255,255,255,0.05);
padding-bottom: 8px;
}
.kv-row:last-child { border-bottom: 0; padding-bottom: 0; }
.kv-label { color: var(--muted); font-size: 0.84rem; }
.kv-value { text-align: right; font-size: 0.88rem; word-break: break-word; }
.snapshot-list { display: grid; gap: 10px; margin-top: 14px; }
.snapshot-item {
border: 1px solid var(--border);
border-radius: 14px;
padding: 12px;
background: rgba(255,255,255,0.02);
}
.snapshot-item strong { display: block; margin-bottom: 4px; font-size: 0.9rem; }
details.more-charts {
border: 1px dashed var(--border);
border-radius: 16px;
padding: 0 14px 14px;
background: rgba(255,255,255,0.015);
}
details.more-charts > summary {
cursor: pointer;
color: var(--accent);
font-weight: 600;
padding: 14px 0;
}
.table-shell { overflow: hidden; }
.table-headline {
display: flex;
justify-content: space-between;
gap: 12px;
align-items: center;
padding: 12px 14px;
border-bottom: 1px solid var(--border);
}
.table-title { font-size: 0.95rem; font-weight: 600; }
.table-note { color: var(--muted); font-size: 0.82rem; }
.table-wrap { overflow: auto; max-height: 720px; }
table { width: 100%; border-collapse: collapse; font-size: 0.89rem; }
th {
position: sticky;
top: 0;
z-index: 2;
text-align: left;
padding: 0.72rem 0.85rem;
background: rgba(17, 24, 39, 0.98);
color: var(--accent);
border-bottom: 1px solid var(--border);
white-space: nowrap;
cursor: pointer;
}
th.sort-active { background: rgba(88, 166, 255, 0.16); color: var(--text); }
td {
padding: 0.68rem 0.85rem;
border-bottom: 1px solid rgba(255,255,255,0.05);
white-space: nowrap;
vertical-align: top;
}
td.truncate-cell {
max-width: 260px;
overflow: hidden;
text-overflow: ellipsis;
}
tr.odd td { background: rgba(255,255,255,0.022); }
tr.even td { background: transparent; }
tr:hover td { background: rgba(88, 166, 255, 0.07); }
td.num { text-align: right; font-variant-numeric: tabular-nums; }
code {
color: var(--text);
background: rgba(255,255,255,0.06);
padding: 0.14rem 0.36rem;
border-radius: 6px;
}
.back-to-top {
position: fixed;
right: 22px;
bottom: 22px;
border-radius: 999px;
border: 1px solid var(--border);
background: rgba(17, 24, 39, 0.86);
color: var(--text);
text-decoration: none;
padding: 0.75rem 0.95rem;
}
@media (max-width: 1280px) {
.stats-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.toolbar-row.primary,
.toolbar-row.secondary {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 980px) {
.layout { grid-template-columns: 1fr; }
.panel { position: static; }
}
@media (max-width: 760px) {
.container { padding: 16px 12px 56px; }
.hero { padding: 22px 18px; }
.toolbar-row.primary,
.toolbar-row.secondary,
.stats-grid {
grid-template-columns: 1fr;
}
}
"""
omni_data_json = json.dumps(list(omni_records), ensure_ascii=False)
diffusion_data_json = json.dumps(list(diffusion_records), ensure_ascii=False)
omni_cols_json = json.dumps(list(omni_columns), ensure_ascii=False)
diffusion_cols_json = json.dumps(list(diffusion_columns), ensure_ascii=False)
script = f"""
const OMNI_COLUMNS = {omni_cols_json};
const DIFF_COLUMNS = {diffusion_cols_json};
const OMNI_DATA = {omni_data_json};
const DIFF_DATA = {diffusion_data_json};
function escapeHtml(value) {{
if (value === null || value === undefined) return "";
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}}
function uniqSorted(arr) {{
const items = new Set(arr.filter(v => v !== null && v !== undefined && String(v).trim() !== ""));
return Array.from(items).sort((a, b) => String(a).localeCompare(String(b)));
}}
function toNumber(v) {{
if (v === null || v === undefined) return null;
if (typeof v === "number") return Number.isFinite(v) ? v : null;
const s = String(v).trim();
if (!s || s.toLowerCase() === "inf") return null;
const n = Number(s);
return Number.isFinite(n) ? n : null;
}}
function fmt(v) {{
const n = toNumber(v);
return n === null ? "" : n.toFixed(4);
}}
function formatDate(v) {{
const s = String(v || "");
const m = s.match(/^(\\d{{4}})(\\d{{2}})(\\d{{2}})-(\\d{{2}})(\\d{{2}})(\\d{{2}})$/);
return m ? `${{m[1]}}-${{m[2]}}-${{m[3]}} ${{m[4]}}:${{m[5]}}:${{m[6]}}` : s;
}}
function shortenModelName(value) {{
const s = String(value || "");
if (!s) return "";
const tail = s.includes("/") ? s.split("/").pop() : s;
return tail
.replace("-Instruct", "")
.replace("-Chat", "")
.replace("-Preview", "");
}}
function shortenDatasetName(value) {{
const s = String(value || "");
if (s === "random") return "rnd";
if (s === "random-mm") return "rnd-mm";
return s;
}}
function compactSeriesLabel(prefix, row, metric, fallback) {{
if (prefix === "omni") {{
const parts = [
row.test_name || "",
shortenDatasetName(row.dataset_name),
row.max_concurrency !== undefined && row.max_concurrency !== null ? `c${{row.max_concurrency}}` : "",
row.num_prompts !== undefined && row.num_prompts !== null ? `p${{row.num_prompts}}` : "",
].filter(Boolean);
return `${{metric}} | ${{parts.join(" | ") || fallback}}`;
}}
const parts = [
row.test_name || "",
shortenModelName(row.model),
shortenDatasetName(row.dataset),
].filter(Boolean);
return `${{metric}} | ${{parts.join(" | ") || fallback}}`;
}}
function compactLegendLabel(prefix, row, metric, fallback) {{
if (prefix === "omni") {{
const parts = [
row.max_concurrency !== undefined && row.max_concurrency !== null ? `c${{row.max_concurrency}}` : "",
row.num_prompts !== undefined && row.num_prompts !== null ? `p${{row.num_prompts}}` : "",
].filter(Boolean);
return `${{metric}} | ${{parts.join(" | ") || fallback}}`;
}}
const parts = [
(row.test_name || "").replace(/^test_/, ""),
shortenModelName(row.model),
].filter(Boolean);
return parts.join(" | ") || fallback;
}}
function truncateCellText(column, value) {{
const raw = value === null || value === undefined ? "" : String(value);
if (!raw) return {{ text: "", title: "" }};
if (column === "model_id" || column === "tokenizer_id" || column === "model") {{
const short = shortenModelName(raw);
return {{
text: short.length > 28 ? `${{short.slice(0, 28)}}...` : short,
title: raw,
}};
}}
if (column === "source_file") {{
return {{
text: raw.length > 38 ? `${{raw.slice(0, 38)}}...` : raw,
title: raw,
}};
}}
if (column === "test_name") {{
return {{
text: raw.length > 32 ? `${{raw.slice(0, 32)}}...` : raw,
title: raw,
}};
}}
if (raw.length > 42) {{
return {{
text: `${{raw.slice(0, 42)}}...`,
title: raw,
}};
}}
return {{ text: raw, title: raw }};
}}
function fillDatalist(el, values) {{
el.innerHTML = "";
values.forEach((v) => {{
const opt = document.createElement("option");
opt.value = String(v);
el.appendChild(opt);
}});
}}
function fillSelect(el, values, selectedValue) {{
if (!el) return;
const previous = selectedValue ?? el.value;
el.innerHTML = "";
const allOption = document.createElement("option");
allOption.value = "";
allOption.textContent = "All";
el.appendChild(allOption);
values.forEach((v) => {{
const opt = document.createElement("option");
opt.value = String(v);
opt.textContent = String(v);
el.appendChild(opt);
}});
if (previous && values.map((v) => String(v)).includes(String(previous))) {{
el.value = String(previous);
}} else {{
el.value = "";
}}
}}
function sortRows(rows, column, desc, numericCols) {{
const next = [...rows];
next.sort((a, b) => {{
let cmp = 0;
if (numericCols.has(column)) {{
cmp = (toNumber(a[column]) ?? -Infinity) - (toNumber(b[column]) ?? -Infinity);
}} else {{
cmp = String(a[column] || "").localeCompare(String(b[column] || ""));
}}
if (cmp === 0) {{
cmp = String(a.date || "").localeCompare(String(b.date || ""));
}}
return desc ? -cmp : cmp;
}});
return next;
}}
function renderStats(container, cards) {{
container.innerHTML = "";
cards.forEach((card) => {{
const el = document.createElement("div");
el.className = "stat-card";
el.innerHTML = `
<div class="stat-label">${{escapeHtml(card.label)}}</div>
<div class="stat-value">${{escapeHtml(card.value)}}</div>
<div class="stat-sub">${{escapeHtml(card.sub || "")}}</div>
`;
container.appendChild(el);
}});
}}
function renderTable(containerId, columns, rows, numericCols, sortState) {{
const container = document.getElementById(containerId);
container.innerHTML = "";
const shell = document.createElement("div");
shell.className = "table-shell";
const headline = document.createElement("div");
headline.className = "table-headline";
headline.innerHTML = `
<div class="table-title">Detailed records</div>
<div class="table-note">${{rows.length}} row(s) shown. Click a header to sort.</div>
`;
const wrap = document.createElement("div");
wrap.className = "table-wrap";
const table = document.createElement("table");
const thead = document.createElement("thead");
const trh = document.createElement("tr");
for (const col of columns) {{
const th = document.createElement("th");
th.textContent = col + (sortState.column === col ? (sortState.desc ? " ↓" : " ↑") : "");
if (sortState.column === col) th.className = "sort-active";
th.addEventListener("click", () => {{
if (sortState.column === col) {{
sortState.desc = !sortState.desc;
}} else {{
sortState.column = col;
sortState.desc = false;
}}
sortState.onChange();
}});
trh.appendChild(th);
}}
thead.appendChild(trh);
table.appendChild(thead);
const tbody = document.createElement("tbody");
rows.forEach((row, idx) => {{
const tr = document.createElement("tr");
tr.className = idx % 2 === 0 ? "even" : "odd";
columns.forEach((col) => {{
const td = document.createElement("td");
if (numericCols.has(col)) {{
td.className = "num";
td.textContent = fmt(row[col]);
}} else {{
const display = col === "date"
? {{ text: formatDate(row[col]), title: formatDate(row[col]) }}
: truncateCellText(col, row[col]);
td.textContent = display.text;
if (display.title && display.title !== display.text) {{
td.title = display.title;
td.classList.add("truncate-cell");
}}
}}
tr.appendChild(td);
}});
tbody.appendChild(tr);
}});
table.appendChild(tbody);
wrap.appendChild(table);
shell.appendChild(headline);
shell.appendChild(wrap);
container.appendChild(shell);
}}
function drawChart(canvas, tooltip, seriesList, labels, legendLabels) {{
const ctx = canvas.getContext("2d");
const rect = canvas.getBoundingClientRect();
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.max(1, Math.floor(rect.width * dpr));
canvas.height = Math.max(1, Math.floor(rect.height * dpr));
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const w = rect.width;
const h = rect.height;
const padLeft = 18;
const padRight = 10;
const padTop = 14;
const padBottom = 64;
const innerW = w - padLeft - padRight;
const innerH = h - padTop - padBottom;
ctx.clearRect(0, 0, w, h);
let yMin = Infinity;
let yMax = -Infinity;
let xLabels = [];
seriesList.forEach((series) => {{
series.forEach((p) => {{
yMin = Math.min(yMin, p.y);
yMax = Math.max(yMax, p.y);
xLabels.push(p.x);
}});
}});
xLabels = uniqSorted(xLabels);
if (!Number.isFinite(yMin) || !Number.isFinite(yMax) || xLabels.length < 2) {{
ctx.fillStyle = "rgba(139,148,158,0.95)";
ctx.font = "13px -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", sans-serif";
ctx.fillText("No data to plot.", 12, 24);
return;
}}
if (yMin === yMax) {{
yMin -= 1;
yMax += 1;
}}
function xScale(x) {{
return padLeft + (xLabels.indexOf(x) / (xLabels.length - 1)) * innerW;
}}
function yScale(y) {{
return padTop + (1 - (y - yMin) / (yMax - yMin)) * innerH;
}}
ctx.strokeStyle = "rgba(45,58,79,0.9)";
ctx.lineWidth = 1;
for (let i = 0; i <= 4; i++) {{
const yy = padTop + (i / 4) * innerH;
ctx.beginPath();
ctx.moveTo(padLeft, yy);
ctx.lineTo(padLeft + innerW, yy);
ctx.stroke();
}}
ctx.font = "11px -apple-system, BlinkMacSystemFont, \\"Segoe UI\\", sans-serif";
ctx.fillStyle = "rgba(139,148,158,0.95)";
for (let i = 0; i <= 4; i++) {{
const yy = padTop + (i / 4) * innerH;
const val = (yMax - ((i / 4) * (yMax - yMin))).toFixed(2);
ctx.fillText(val, padLeft + 4, yy - 4);
}}
const tickLabels = xLabels.length <= 3
? xLabels
: [xLabels[0], xLabels[Math.floor((xLabels.length - 1) / 2)], xLabels[xLabels.length - 1]];
tickLabels.forEach((tick) => {{
const xx = xScale(tick);
ctx.fillText(formatDate(tick).slice(5, 16), Math.max(0, xx - 28), h - 26);
}});
const colors = ["#58a6ff", "#3fb950", "#f0883e", "#d2a8ff", "#ff7b72", "#79c0ff"];
seriesList.forEach((series, idx) => {{
ctx.strokeStyle = colors[idx % colors.length];
ctx.lineWidth = 2;
ctx.beginPath();
series.forEach((p, pointIdx) => {{
const xx = xScale(p.x);
const yy = yScale(p.y);
if (pointIdx === 0) ctx.moveTo(xx, yy);
else ctx.lineTo(xx, yy);
}});
ctx.stroke();
series.forEach((p) => {{
const xx = xScale(p.x);
const yy = yScale(p.y);
ctx.fillStyle = colors[idx % colors.length];
ctx.beginPath();
ctx.arc(xx, yy, 3, 0, Math.PI * 2);
ctx.fill();
}});
}});
legendLabels.slice(0, 4).forEach((label, idx) => {{
ctx.fillStyle = colors[idx % colors.length];
ctx.fillRect(padLeft + idx * 180, h - 14, 10, 10);
ctx.fillStyle = "rgba(230,237,243,0.95)";
const shortLabel = label.length > 22 ? `${{label.slice(0, 22)}}...` : label;
ctx.fillText(shortLabel, padLeft + idx * 180 + 14, h - 5);
}});
function hideTooltip() {{
tooltip.style.display = "none";
}}
function showTooltip(clientX, clientY, html) {{
tooltip.innerHTML = html;
tooltip.style.display = "block";
const parentRect = canvas.parentElement.getBoundingClientRect();
tooltip.style.left = `${{Math.min(clientX - parentRect.left + 10, parentRect.width - 320)}}px`;
tooltip.style.top = `${{Math.min(clientY - parentRect.top + 10, parentRect.height - 120)}}px`;
}}
canvas.onmousemove = (ev) => {{
const cRect = canvas.getBoundingClientRect();
const x = ev.clientX - cRect.left;
const y = ev.clientY - cRect.top;
if (x < padLeft || x > padLeft + innerW || y < padTop || y > padTop + innerH) {{
hideTooltip();
return;
}}
const rel = (x - padLeft) / innerW;
const idx = Math.round(rel * (xLabels.length - 1));
const xVal = xLabels[Math.max(0, Math.min(xLabels.length - 1, idx))];
let best = null;
seriesList.forEach((series, seriesIdx) => {{
const point = series.find((p) => p.x === xVal);
if (!point) return;
const yy = yScale(point.y);
const dist = Math.abs(yy - y);
if (!best || dist < best.dist) {{
best = {{ dist, point, seriesIdx }};
}}
}});
if (!best) {{
hideTooltip();
return;
}}
const metaLines = Object.entries(best.point.meta || {{}})
.filter(([, value]) => value !== null && value !== undefined && String(value).trim() !== "")
.map(
([key, value]) =>
`<div class="muted">${{escapeHtml(key)}}: ` +
`<code>${{escapeHtml(String(value))}}</code></div>`,
)
.join("");
showTooltip(ev.clientX, ev.clientY, `
<div><strong>${{escapeHtml(labels[best.seriesIdx] || "")}}</strong></div>
<div class="muted">date: <code>${{escapeHtml(formatDate(best.point.x))}}</code></div>
<div class="muted">value: <code>${{best.point.y.toFixed(4)}}</code></div>
${{metaLines}}
`);
}};
canvas.onmouseleave = hideTooltip;
}}
function renderChartGroups(container, rowsAsc, groups, metaKeys, configFields, labelFields, maxVisible, prefix) {{
container.innerHTML = "";
const visible = groups.slice(0, maxVisible);
const extra = groups.slice(maxVisible);
function renderInto(groupList, parent) {{
groupList.forEach((group) => {{
const seriesByKey = new Map();
rowsAsc.forEach((row) => {{
const dateStr = String(row.date || "");
if (!dateStr) return;
const cfgParts = configFields
.map((field) => row[field])
.filter((value) => value !== null && value !== undefined && String(value).trim() !== "")
.map((value) => String(value));
const cfgKey = cfgParts.length ? cfgParts.join("||") : "config";
group.metrics.forEach((metric) => {{
const y = toNumber(row[metric]);
if (y === null) return;
const key = `${{metric}}||${{cfgKey}}`;
if (!seriesByKey.has(key)) {{
const fallback = cfgKey.replaceAll("||", " | ");
seriesByKey.set(key, {{
label: compactSeriesLabel(prefix, row, metric, fallback),
legend: compactLegendLabel(prefix, row, metric, fallback),
points: [],
}});
}}
const meta = {{}};
metaKeys.forEach((keyName) => {{ meta[keyName] = row[keyName]; }});
seriesByKey.get(key).points.push({{ x: dateStr, y, meta }});
}});
}});
const seriesEntries = Array.from(seriesByKey.values()).filter(
(entry) => entry.points.length > 0,
);
if (!seriesEntries.length) return;
const seriesList = seriesEntries.map((entry) => entry.points);
const labels = seriesEntries.map((entry) => entry.label);
const legendLabels = seriesEntries.map((entry) => entry.legend);
const totalPoints = seriesList.reduce((acc, series) => acc + series.length, 0);
const card = document.createElement("div");
card.className = "chart-card";
const head = document.createElement("div");
head.className = "chart-head";
head.innerHTML =
`<div class="chart-title">${{escapeHtml(group.title)}}` +
`${{totalPoints < 2 ? " (snapshot)" : ""}}</div>` +
`<div class="chart-caption">${{seriesList.length}} series · ` +
`${{totalPoints}} point(s)</div>`;
card.appendChild(head);
if (totalPoints < 2) {{
const latest = rowsAsc[rowsAsc.length - 1];
const body = document.createElement("div");
body.className = "hint";
const lines = [];
if (latest && latest.date) {{
lines.push(`date: <code>${{escapeHtml(formatDate(latest.date))}}</code>`);
}}
metaKeys.forEach((keyName) => {{
if (
!latest ||
latest[keyName] === null ||
latest[keyName] === undefined ||
String(latest[keyName]).trim() === ""
) {{
return;
}}
lines.push(`${{escapeHtml(keyName)}}: <code>${{escapeHtml(String(latest[keyName]))}}</code>`);
}});
group.metrics.forEach((metric) => {{
const value = latest ? fmt(latest[metric]) : "";
if (value) lines.push(`${{escapeHtml(metric)}}: <code>${{value}}</code>`);
}});
body.innerHTML = lines.join("<br>") || "No numeric data for snapshot.";
card.appendChild(body);
parent.appendChild(card);
return;
}}
const wrap = document.createElement("div");
wrap.className = "chart-wrap";
const canvas = document.createElement("canvas");
const tooltip = document.createElement("div");
tooltip.className = "chart-tooltip";
wrap.appendChild(canvas);
wrap.appendChild(tooltip);
card.appendChild(wrap);
parent.appendChild(card);
canvas.__draw = () => {{
const r = canvas.getBoundingClientRect();
if (!r || r.width < 10 || r.height < 10) return;
drawChart(canvas, tooltip, seriesList, labels, legendLabels);
}};
requestAnimationFrame(() => canvas.__draw());
}});
}}
renderInto(visible, container);
if (extra.length) {{
const details = document.createElement("details");
details.className = "more-charts";
const summary = document.createElement("summary");
summary.textContent = `More charts (${{extra.length}})`;
details.appendChild(summary);
const inner = document.createElement("div");
details.appendChild(inner);
renderInto(extra, inner);
details.addEventListener("toggle", () => {{
if (!details.open) return;
requestAnimationFrame(() => {{
details.querySelectorAll("canvas").forEach((canvas) => {{
if (typeof canvas.__draw === "function") canvas.__draw();
}});
}});
}});
container.appendChild(details);
}}
if (!container.querySelector(".chart-card")) {{
const empty = document.createElement("div");
empty.className = "chart-card empty";
empty.textContent = "No data to plot for current filters.";
container.appendChild(empty);
}}
}}
function filterRows(rows, filters) {{
return rows.filter((row) => {{
if (
filters.modelSearch &&
!String(row[filters.modelKey] || "")
.toLowerCase()
.includes(filters.modelSearch.toLowerCase())
) {{
return false;
}}
if (filters.model && String(row[filters.modelKey] || "") !== filters.model) return false;
if (filters.testName && String(row.test_name || "") !== filters.testName) return false;
if (filters.datasetName && String(row[filters.datasetKey] || "") !== filters.datasetName) return false;
if (
filters.maxConcurrency &&
String(row.max_concurrency || "") !== filters.maxConcurrency
) {{
return false;
}}
if (
filters.numPrompts &&
String(row.num_prompts || "") !== filters.numPrompts
) {{
return false;
}}
if (filters.backend && String(row[filters.backendKey] || "") !== filters.backend) return false;
if (
filters.extraKey &&
filters.extraValue &&
String(row[filters.extraKey] || "") !== filters.extraValue
) {{
return false;
}}
return true;
}});
}}
function sortByDateAsc(rows) {{
return [...rows].sort((a, b) => String(a.date || "").localeCompare(String(b.date || "")));
}}
function averageMetric(rows, key) {{
const values = rows.map((row) => toNumber(row[key])).filter((v) => v !== null);
if (!values.length) return "n/a";
const avg = values.reduce((acc, value) => acc + value, 0) / values.length;
return avg.toFixed(4);
}}
function initSection(prefix, columns, data, numericCols, groups) {{
const modelInput = document.getElementById(`${{prefix}}-model`);
const modelSearchInput = document.getElementById(`${{prefix}}-model-search`);
const testInput = document.getElementById(`${{prefix}}-test`);
const datasetInput = document.getElementById(`${{prefix}}-dataset`);
const concurrencyInput = document.getElementById(`${{prefix}}-concurrency`);
const promptsInput = document.getElementById(`${{prefix}}-prompts`);
const backendInput = document.getElementById(`${{prefix}}-backend`);
const extraInput = document.getElementById(`${{prefix}}-extra`);
const statsEl = document.getElementById(`${{prefix}}-stats`);
const panelEl = document.getElementById(`${{prefix}}-panel`);
const chartsEl = document.getElementById(`${{prefix}}-charts`);
const clearBtn = document.getElementById(`${{prefix}}-clear`);
const sortState = {{ column: "date", desc: true, onChange: render }};
const modelKey = prefix === "diff" ? "model" : "model_id";
const datasetKey = prefix === "diff" ? "dataset" : "dataset_name";
const backendKey = prefix === "diff" ? "endpoint" : "backend";
const backendLabel = prefix === "diff" ? "Endpoint" : "Backend";
const extraKey = prefix === "diff" ? "" : "tokenizer_id";
const metaKeys = prefix === "diff"
? ["test_name"]
: ["test_name", "max_concurrency", "num_prompts"];
const configFields = prefix === "diff"
? ["test_name", "model", "endpoint", "dataset"]
: [
"endpoint_type",
"backend",
"model_id",
"tokenizer_id",
"test_name",
"dataset_name",
"max_concurrency",
"num_prompts",
];
const labelFields = prefix === "diff"
? ["test_name", "dataset"]
: ["test_name", "dataset_name"];
const latestMetric = prefix === "diff" ? "throughput_qps" : "output_throughput";
const secondaryMetric = prefix === "diff" ? "latency_mean" : "mean_e2el_ms";
const modelList = document.getElementById(`${{prefix}}-model-list`);
const testList = document.getElementById(`${{prefix}}-test`);
const datasetList = document.getElementById(`${{prefix}}-dataset`);
const concurrencyList = document.getElementById(`${{prefix}}-concurrency`);
const promptsList = document.getElementById(`${{prefix}}-prompts`);
const backendList = document.getElementById(`${{prefix}}-backend`);
const extraList = document.getElementById(`${{prefix}}-extra`);
function currentFilters() {{
return {{
model: modelInput.value.trim(),
modelSearch: modelSearchInput ? modelSearchInput.value.trim() : "",
testName: testInput.value.trim(),
datasetName: datasetInput.value.trim(),
maxConcurrency: concurrencyInput ? concurrencyInput.value.trim() : "",
numPrompts: promptsInput ? promptsInput.value.trim() : "",
backend: backendInput.value.trim(),
extraValue: extraInput ? extraInput.value.trim() : "",
extraKey,
modelKey,
datasetKey,
backendKey,
}};
}}
function refreshOptionLists(baseRows) {{
fillDatalist(modelList, uniqSorted(baseRows.map((row) => row[modelKey])));
fillSelect(
testList,
uniqSorted(baseRows.map((row) => row.test_name)),
testInput.value,
);
fillSelect(
datasetList,
uniqSorted(baseRows.map((row) => row[datasetKey])),
datasetInput.value,
);
if (concurrencyList) {{
fillSelect(
concurrencyList,
uniqSorted(baseRows.map((row) => row.max_concurrency)),
concurrencyInput.value,
);
}}
if (promptsList) {{
fillSelect(
promptsList,
uniqSorted(baseRows.map((row) => row.num_prompts)),
promptsInput.value,
);
}}
fillSelect(
backendList,
uniqSorted(baseRows.map((row) => row[backendKey])),
backendInput.value,
);
if (extraList) {{
fillSelect(
extraList,
uniqSorted(baseRows.map((row) => row[extraKey])),
extraInput.value,
);
}}
}}
function firstValue(rows, key) {{
const values = uniqSorted(rows.map((row) => row[key]));
return values.length ? String(values[0]) : "";
}}
function applyDefaultSelections() {{
if (!data.length) return;
let rows = data;
if (!modelInput.value.trim()) {{
modelInput.value = firstValue(rows, modelKey);
}}
rows = filterRows(data, {{
...currentFilters(),
testName: "",
datasetName: "",
maxConcurrency: "",
numPrompts: "",
backend: "",
extraValue: "",
modelSearch: "",
}});
if (!testInput.value.trim()) {{
testInput.value = firstValue(rows, "test_name");
}}
rows = filterRows(data, {{
...currentFilters(),
datasetName: "",
maxConcurrency: "",
numPrompts: "",
backend: "",
extraValue: "",
modelSearch: "",
}});
if (!datasetInput.value.trim()) {{
datasetInput.value = firstValue(rows, datasetKey);
}}
rows = filterRows(data, {{
...currentFilters(),
maxConcurrency: "",
numPrompts: "",
backend: "",
extraValue: "",
modelSearch: "",
}});
if (concurrencyInput && !concurrencyInput.value.trim()) {{
concurrencyInput.value = firstValue(rows, "max_concurrency");
}}
rows = filterRows(data, {{
...currentFilters(),
numPrompts: "",
backend: "",
extraValue: "",
modelSearch: "",
}});
if (promptsInput && !promptsInput.value.trim()) {{
promptsInput.value = firstValue(rows, "num_prompts");
}}
rows = filterRows(data, {{
...currentFilters(),
backend: "",
extraValue: "",
modelSearch: "",
}});
if (!backendInput.value.trim()) {{
backendInput.value = firstValue(rows, backendKey);
}}
rows = filterRows(data, {{
...currentFilters(),
extraValue: "",
modelSearch: "",
}});
if (extraKey && extraInput && !extraInput.value.trim()) {{
extraInput.value = firstValue(rows, extraKey);
}}
}}
refreshOptionLists(data);
applyDefaultSelections();
function render() {{
const filters = currentFilters();
const filtered = filterRows(data, filters);
const filteredAsc = sortByDateAsc(filtered);
const latest = filteredAsc[filteredAsc.length - 1] || null;
const configCount = new Set(
filtered.map((row) =>
configFields.map((field) => String(row[field] || "")).join("||"),
),
).size;
refreshOptionLists(filterRows(data, {{
...filters,
testName: "",
datasetName: "",
maxConcurrency: "",
numPrompts: "",
backend: "",
extraValue: "",
modelSearch: "",
}}));
renderStats(statsEl, [
{{ label: "filtered records", value: String(filtered.length), sub: `${{configCount}} unique config key(s)` }},
{{
label: "latest run",
value: latest ? formatDate(latest.date) : "n/a",
sub: "newest visible point",
}},
{{
label: `latest ${{latestMetric}}`,
value: latest ? (fmt(latest[latestMetric]) || "n/a") : "n/a",
sub: latestMetric,
}},
{{
label: `avg ${{secondaryMetric}}`,
value: averageMetric(filtered, secondaryMetric),
sub: secondaryMetric,
}},
]);
panelEl.innerHTML = `
<h3>Selection details</h3>
<div class="kv">
<div class="kv-row"><div class="kv-label">Model</div><div class="kv-value"><code>${{escapeHtml(
filters.model || "All",
)}}</code></div></div>
<div class="kv-row"><div class="kv-label">Model search</div><div class="kv-value"><code>${{escapeHtml(
filters.modelSearch || "All",
)}}</code></div></div>
<div class="kv-row"><div class="kv-label">Test</div><div class="kv-value"><code>${{escapeHtml(
filters.testName || "All",
)}}</code></div></div>
<div class="kv-row"><div class="kv-label">Dataset</div><div class="kv-value"><code>${{escapeHtml(
filters.datasetName || "All",
)}}</code></div></div>
${{
prefix === "diff"
? ""
: (
`<div class="kv-row"><div class="kv-label">Concurrency</div>` +
`<div class="kv-value"><code>${{escapeHtml(filters.maxConcurrency || "All")}}</code></div></div>`
)
}}
${{
prefix === "diff"
? ""
: (
`<div class="kv-row"><div class="kv-label">Num prompts</div>` +
`<div class="kv-value"><code>${{escapeHtml(filters.numPrompts || "All")}}</code></div></div>`
)
}}
<div class="kv-row"><div class="kv-label">${{backendLabel}}</div><div class="kv-value"><code>${{escapeHtml(
filters.backend || "All",
)}}</code></div></div>
${{
prefix === "diff"
? ""
: (
`<div class="kv-row"><div class="kv-label">Tokenizer</div>` +
`<div class="kv-value"><code>${{escapeHtml(filters.extraValue || "All")}}</code></div></div>`
)
}}
<div class="kv-row"><div class="kv-label">Latest source</div><div class="kv-value"><code>${{
escapeHtml(latest ? String(latest.source_file || "") : "n/a")
}}</code></div></div>
</div>
<div class="snapshot-list">
${{
latest ? [
latest[latestMetric] !== undefined
? `<div class="snapshot-item"><strong>${{escapeHtml(
latestMetric,
)}}</strong><div>${{fmt(latest[latestMetric]) || "n/a"}}</div></div>`
: "",
latest[secondaryMetric] !== undefined
? `<div class="snapshot-item"><strong>${{escapeHtml(
secondaryMetric,
)}}</strong><div>${{fmt(latest[secondaryMetric]) || "n/a"}}</div></div>`
: "",
latest.date
? `<div class="snapshot-item"><strong>date</strong><div>${{
escapeHtml(formatDate(latest.date))
}}</div></div>`
: "",
].join("") : '<div class="hint">No visible snapshot.</div>'
}}
</div>
`;
if (prefix === "omni" && !filters.model) {{
chartsEl.innerHTML =
"<div class='chart-card empty'>Select a <code>model_id</code> " +
"to focus the Omni trend view.</div>";
}} else {{
renderChartGroups(chartsEl, filteredAsc, groups, metaKeys, configFields, labelFields, 3, prefix);
}}
renderTable(
`${{prefix}}-table`,
columns,
sortRows(filteredAsc, sortState.column, sortState.desc, numericCols),
numericCols,
sortState,
);
}}
[
modelInput,
modelSearchInput,
testInput,
datasetInput,
concurrencyInput,
promptsInput,
backendInput,
extraInput,
].filter(Boolean).forEach((el) => {{
el.addEventListener("input", render);
el.addEventListener("change", render);
}});
clearBtn.addEventListener("click", () => {{
modelInput.value = "";
if (modelSearchInput) modelSearchInput.value = "";
testInput.value = "";
datasetInput.value = "";
if (concurrencyInput) concurrencyInput.value = "";
if (promptsInput) promptsInput.value = "";
backendInput.value = "";
if (extraInput) extraInput.value = "";
applyDefaultSelections();
render();
}});
render();
}}
window.addEventListener("load", () => {{
const omniNumeric = new Set([
"num_prompts", "burstiness", "max_concurrency", "duration", "completed", "failed",
"request_throughput", "output_throughput", "total_token_throughput",
"mean_ttft_ms", "median_ttft_ms", "p99_ttft_ms",
"mean_tpot_ms", "median_tpot_ms", "p99_tpot_ms",
"mean_itl_ms", "median_itl_ms", "p99_itl_ms",
"mean_e2el_ms", "median_e2el_ms", "p99_e2el_ms",
"mean_audio_rtf", "median_audio_rtf", "p99_audio_rtf",
"mean_audio_ttfp_ms", "median_audio_ttfp_ms", "p99_audio_ttfp_ms",
"mean_audio_duration_s", "median_audio_duration_s", "p99_audio_duration_s",
]);
const diffNumeric = new Set([
"duration", "completed_requests", "failed_requests", "throughput_qps",
"latency_mean", "latency_median", "latency_p50", "latency_p99",
"peak_memory_mb_max", "peak_memory_mb_mean", "peak_memory_mb_median", "slo_attainment_rate",
]);
const omniGroups = [
{{ title: "throughput", metrics: ["output_throughput", "total_token_throughput"] }},
{{ title: "ttft", metrics: ["mean_ttft_ms", "median_ttft_ms", "p99_ttft_ms"] }},
{{
title: "tpot + itl",
metrics: [
"mean_tpot_ms",
"median_tpot_ms",
"p99_tpot_ms",
"mean_itl_ms",
"median_itl_ms",
"p99_itl_ms",
],
}},
{{ title: "e2el", metrics: ["mean_e2el_ms", "median_e2el_ms", "p99_e2el_ms"] }},
{{ title: "audio rtf", metrics: ["mean_audio_rtf", "median_audio_rtf", "p99_audio_rtf"] }},
{{ title: "audio ttfp", metrics: ["mean_audio_ttfp_ms", "median_audio_ttfp_ms", "p99_audio_ttfp_ms"] }},
{{
title: "audio duration",
metrics: [
"mean_audio_duration_s",
"median_audio_duration_s",
"p99_audio_duration_s",
],
}},
];
const diffGroups = [
{{ title: "throughput", metrics: ["throughput_qps"] }},
{{
title: "latency",
metrics: [
"latency_mean",
"latency_median",
"latency_p99",
"latency_p50",
],
}},
];
initSection("omni", OMNI_COLUMNS, OMNI_DATA, omniNumeric, omniGroups);
initSection("diff", DIFF_COLUMNS, DIFF_DATA, diffNumeric, diffGroups);
}});
"""
return "\n".join(
[
"<!DOCTYPE html>",
'<html lang="en">',
"<head>",
' <meta charset="utf-8" />',
' <meta name="viewport" content="width=device-width, initial-scale=1" />',
" <title>Nightly Performance Report V2</title>",
f" <style>{styles}</style>",
"</head>",
"<body>",
' <div class="container" id="top">',
' <div class="hero">',
" <h1>Nightly Performance Report</h1>",
(
' <div class="meta">Improved dashboard view for nightly Omni '
"and Diffusion benchmarks. The page keeps the current dark color "
"scheme, but adds stronger filtering, summary cards, sticky "
"tables, and more readable trend blocks.</div>"
),
' <div class="hero-badges">',
f' <div class="badge">Omni records <strong>{len(omni_records)}</strong></div>',
f' <div class="badge">Diffusion records <strong>{len(diffusion_records)}</strong></div>',
' <div class="badge">Numeric precision <strong>4 decimals</strong></div>',
" </div>",
" </div>",
' <section id="omni-section">',
(
' <div class="section-head"><div><h2 class="section-title">Omni</h2>'
'<div class="section-subtitle">Chat/audio benchmark history with '
"trend charts grouped by throughput, latency, and audio metrics."
'</div></div><div class="section-pills"><div class="pill"><strong>'
"Grouping</strong> test_name + dataset + concurrency + prompts</div>"
'<div class="pill"><strong>Snapshots</strong> single-point series '
"stay readable</div></div></div>"
),
' <div class="stats-grid" id="omni-stats"></div>',
' <div class="toolbar">',
' <div class="toolbar-row primary">',
(
' <div class="filter-field"><label>model_id</label><input '
'class="filter-input" type="text" id="omni-model" '
'list="omni-model-list" placeholder="Select model_id" /><datalist '
'id="omni-model-list"></datalist></div>'
),
(
' <div class="filter-field compact"><label>search</label><input '
'class="filter-input" type="text" id="omni-model-search" '
'placeholder="Fuzzy search model_id" /></div>'
),
(
' <div class="filter-field compact"><label>test</label>'
'<select class="filter-select" id="omni-test"></select></div>'
),
(
' <div class="filter-field compact"><label>dataset</label>'
'<select class="filter-select" id="omni-dataset"></select></div>'
),
" </div>",
' <div class="toolbar-row secondary">',
(
' <div class="filter-field compact"><label>max_conc</label>'
'<select class="filter-select" id="omni-concurrency"></select></div>'
),
(
' <div class="filter-field compact"><label>prompts</label>'
'<select class="filter-select" id="omni-prompts"></select></div>'
),
(
' <div class="filter-field compact"><label>backend</label>'
'<select class="filter-select" id="omni-backend"></select></div>'
),
(
' <div class="filter-field compact"><label>tokenizer</label>'
'<select class="filter-select" id="omni-extra"></select></div>'
),
(
' <div class="filter-field compact"><label>&nbsp;</label><button '
'class="ghost" type="button" id="omni-clear">Clear filters</button>'
"</div>"
),
" </div>",
" </div>",
(
' <div class="hint">Searchable inputs use browser datalist '
"suggestions for quick keyboard selection.</div>"
),
(
' <div class="layout"><div class="charts-stack" '
'id="omni-charts"></div><div class="panel" id="omni-panel"></div>'
"</div>"
),
' <div id="omni-table"></div>',
" </section>",
' <section id="diff-section">',
(
' <div class="section-head"><div><h2 class="section-title">'
'Diffusion</h2><div class="section-subtitle">Image generation '
"benchmark history with throughput and latency trend views.</div>"
'</div><div class="section-pills"><div class="pill"><strong>'
"Grouping</strong> test_name + model + endpoint + dataset</div>"
'<div class="pill"><strong>Snapshots</strong> single-point series '
"stay readable</div></div></div>"
),
' <div class="stats-grid" id="diff-stats"></div>',
' <div class="toolbar">',
' <div class="toolbar-row primary">',
(
' <div class="filter-field"><label>model</label><input '
'class="filter-input" type="text" id="diff-model" '
'list="diff-model-list" placeholder="Select model" /><datalist '
'id="diff-model-list"></datalist></div>'
),
(
' <div class="filter-field compact"><label>search</label><input '
'class="filter-input" type="text" id="diff-model-search" '
'placeholder="Fuzzy search model" /></div>'
),
(
' <div class="filter-field compact"><label>test</label>'
'<select class="filter-select" id="diff-test"></select></div>'
),
(
' <div class="filter-field compact"><label>dataset</label>'
'<select class="filter-select" id="diff-dataset"></select></div>'
),
" </div>",
' <div class="toolbar-row secondary">',
(
' <div class="filter-field compact"><label>endpoint</label>'
'<select class="filter-select" id="diff-backend"></select></div>'
),
(
' <div class="filter-field compact"><label>&nbsp;</label><button '
'class="ghost" type="button" id="diff-clear">Clear filters</button>'
"</div>"
),
" </div>",
" </div>",
(
' <div class="hint">Charts automatically skip empty metric '
"groups and collapse anything beyond the first three groups.</div>"
),
(
' <div class="layout"><div class="charts-stack" '
'id="diff-charts"></div><div class="panel" id="diff-panel"></div>'
"</div>"
),
' <div id="diff-table"></div>',
" </section>",
" </div>",
' <a class="back-to-top" href="#top">Back to top</a>',
f" <script>{script}</script>",
"</body>",
"</html>",
]
)
def generate_html_report(input_dir: str, diffusion_input_dir: str, output_file: str) -> None:
omni_records = _sort_omni_records(_collect_omni_records(input_dir))
diffusion_records = _sort_diffusion_records(_collect_diffusion_records(diffusion_input_dir))
if not omni_records:
LOGGER.warning("no valid omni json records found under '%s'", input_dir)
if not diffusion_records:
LOGGER.warning("no valid diffusion json records found under '%s'", diffusion_input_dir)
omni_columns = [
"date",
"endpoint_type",
"backend",
"model_id",
"tokenizer_id",
"test_name",
"dataset_name",
"max_concurrency",
"num_prompts",
"request_throughput",
"output_throughput",
"total_token_throughput",
"mean_ttft_ms",
"p99_ttft_ms",
"mean_e2el_ms",
"p99_e2el_ms",
"mean_audio_rtf",
"p99_audio_rtf",
"duration",
"completed",
"failed",
"source_file",
]
diffusion_columns = [
"date",
"test_name",
"model",
"endpoint",
"dataset",
"task",
"duration",
"throughput_qps",
"latency_mean",
"latency_median",
"latency_p50",
"latency_p99",
"completed_requests",
"failed_requests",
"peak_memory_mb_max",
"peak_memory_mb_mean",
"peak_memory_mb_median",
"slo_attainment_rate",
"source_file",
]
html = _build_html_document(
omni_columns=omni_columns,
omni_records=omni_records,
diffusion_columns=diffusion_columns,
diffusion_records=diffusion_records,
)
_ensure_parent_dir(output_file)
with open(output_file, "w", encoding="utf-8") as f:
f.write(html)
LOGGER.info("improved html report saved to '%s'", output_file)
def main() -> None:
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)
args = parse_args()
diffusion_input_dir = args.diffusion_input_dir or _default_diffusion_input_dir(args.input_dir)
generate_html_report(
input_dir=args.input_dir,
diffusion_input_dir=diffusion_input_dir,
output_file=args.output_file,
)
if __name__ == "__main__":
main()