feat: add usage stats key renderer
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { type GpuMetricKind } from "./metrics-parser";
|
||||
import { type ModelRuntimeState } from "./inflight-tracker";
|
||||
import { type UsageStats } from "./stats";
|
||||
import { escapeXml, shorten } from "./util";
|
||||
|
||||
export function svgDataUrl(svg: string): string {
|
||||
@@ -188,3 +189,69 @@ function frame(bg: string, parts: string[]): string {
|
||||
${parts.join("\n ")}
|
||||
</svg>`;
|
||||
}
|
||||
|
||||
export function formatCompact(n: number): string {
|
||||
if (!Number.isFinite(n)) return "--";
|
||||
if (n < 1000) return `${Math.round(n)}`;
|
||||
if (n < 1_000_000) return `${(n / 1000).toFixed(1)}k`;
|
||||
return `${(n / 1_000_000).toFixed(1)}M`;
|
||||
}
|
||||
|
||||
export interface UsageRenderOptions {
|
||||
modelName: string;
|
||||
stats?: UsageStats;
|
||||
primaryStat: "requests" | "input_tokens" | "output_tokens" | "gen_p95";
|
||||
offline: boolean;
|
||||
}
|
||||
|
||||
export function renderUsage(opts: UsageRenderOptions): string {
|
||||
const name = escapeXml(shorten(opts.modelName === "all" ? "ALL MODELS" : opts.modelName));
|
||||
|
||||
if (opts.offline) {
|
||||
return frame("#10131a", [
|
||||
centerText("!!", 34, 20, "bold", "#e0e0e0"),
|
||||
centerText("OFFLINE", 52, 9, "normal", "#bdbdbd"),
|
||||
centerText(name, 64, 7, "normal", "#ffffff"),
|
||||
]);
|
||||
}
|
||||
|
||||
const s = opts.stats;
|
||||
const big = s ? formatCompact(primaryValue(s, opts.primaryStat)) : "--";
|
||||
const rows = usageRows(s, opts.primaryStat);
|
||||
const parts: string[] = [
|
||||
centerText(name, 10, 7, "normal", "#ffffff"),
|
||||
centerText(big, 36, 24, "bold", "#ffffff"),
|
||||
];
|
||||
for (const row of rows) {
|
||||
parts.push(
|
||||
`<text x="4" y="${row.y}" font-family="Arial,sans-serif" font-size="7" fill="#8b93a5">${row.label}</text>`,
|
||||
`<text x="68" y="${row.y}" text-anchor="end" font-family="Arial,sans-serif" font-size="7" fill="#ffffff">${row.value}</text>`,
|
||||
);
|
||||
}
|
||||
return frame("#10131a", parts);
|
||||
}
|
||||
|
||||
function primaryValue(s: UsageStats, primary: "requests" | "input_tokens" | "output_tokens" | "gen_p95"): number {
|
||||
switch (primary) {
|
||||
case "requests":
|
||||
return s.totalRequests;
|
||||
case "input_tokens":
|
||||
return s.totalInputTokens;
|
||||
case "output_tokens":
|
||||
return s.totalOutputTokens;
|
||||
case "gen_p95":
|
||||
return s.genP95;
|
||||
}
|
||||
}
|
||||
|
||||
function usageRows(
|
||||
s: UsageStats | undefined,
|
||||
primary: "requests" | "input_tokens" | "output_tokens" | "gen_p95",
|
||||
): { label: string; value: string; y: number }[] {
|
||||
const items: { label: string; value: string }[] = [];
|
||||
if (primary !== "requests") items.push({ label: "REQ", value: s ? formatCompact(s.totalRequests) : "--" });
|
||||
if (primary !== "input_tokens") items.push({ label: "IN", value: s ? formatCompact(s.totalInputTokens) : "--" });
|
||||
if (primary !== "output_tokens") items.push({ label: "OUT", value: s ? formatCompact(s.totalOutputTokens) : "--" });
|
||||
if (primary !== "gen_p95") items.push({ label: "P95", value: s ? `${Math.round(s.genP95)} t/s` : "--" });
|
||||
return items.map((it, i) => ({ ...it, y: 52 + i * 7 }));
|
||||
}
|
||||
|
||||
+35
-1
@@ -1,6 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { renderGpuGraph, renderInflight, svgDataUrl } from "../src/lib/render";
|
||||
import { renderGpuGraph, renderInflight, renderUsage, svgDataUrl, formatCompact } from "../src/lib/render";
|
||||
|
||||
test("svgDataUrl wraps an SVG as a base64 data URL", () => {
|
||||
const url = svgDataUrl("<svg></svg>");
|
||||
@@ -99,3 +99,37 @@ test("renderGpuGraph: single-point history avoids divide-by-zero", () => {
|
||||
const svg = renderGpuGraph({ gpuName: "ALL GPUS", metric: "util_percent", value: 50, history: [50], offline: false });
|
||||
assert.match(svg, /<svg/);
|
||||
});
|
||||
|
||||
test("formatCompact renders integers, k, and M", () => {
|
||||
assert.equal(formatCompact(0), "0");
|
||||
assert.equal(formatCompact(999), "999");
|
||||
assert.equal(formatCompact(52100), "52.1k");
|
||||
assert.equal(formatCompact(1200000), "1.2M");
|
||||
});
|
||||
|
||||
test("renderUsage shows the primary stat big and the rest small", () => {
|
||||
const svg = renderUsage({
|
||||
modelName: "DeepSeek-V4-Flash-0731",
|
||||
stats: { totalRequests: 1950, totalInputTokens: 312177540, totalOutputTokens: 889193, genP95: 378.86 },
|
||||
primaryStat: "gen_p95",
|
||||
offline: false,
|
||||
});
|
||||
assert.match(svg, />379</);
|
||||
assert.match(svg, /REQ/);
|
||||
assert.match(svg, /IN/);
|
||||
assert.match(svg, /OUT/);
|
||||
assert.match(svg, /312.2M/);
|
||||
assert.match(svg, /#10131a/);
|
||||
});
|
||||
|
||||
test("renderUsage renders -- when stats are unknown and OFFLINE when offline", () => {
|
||||
const none = renderUsage({ modelName: "A", stats: undefined, primaryStat: "requests", offline: false });
|
||||
assert.match(none, />--</);
|
||||
const off = renderUsage({ modelName: "A", stats: undefined, primaryStat: "requests", offline: true });
|
||||
assert.match(off, /OFFLINE/);
|
||||
});
|
||||
|
||||
test("renderUsage labels the aggregate view ALL MODELS", () => {
|
||||
const svg = renderUsage({ modelName: "all", stats: { totalRequests: 1, totalInputTokens: 1, totalOutputTokens: 1, genP95: 1 }, primaryStat: "requests", offline: false });
|
||||
assert.match(svg, /ALL MODELS/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user