diff --git a/src/lib/render.ts b/src/lib/render.ts index 7139787..bf3d30b 100644 --- a/src/lib/render.ts +++ b/src/lib/render.ts @@ -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 ")} `; } + +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( + `${row.label}`, + `${row.value}`, + ); + } + 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 })); +} diff --git a/tests/render.test.ts b/tests/render.test.ts index 45d2e52..6692d6d 100644 --- a/tests/render.test.ts +++ b/tests/render.test.ts @@ -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(""); @@ -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, / { + 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 { + const none = renderUsage({ modelName: "A", stats: undefined, primaryStat: "requests", offline: false }); + assert.match(none, />-- { + const svg = renderUsage({ modelName: "all", stats: { totalRequests: 1, totalInputTokens: 1, totalOutputTokens: 1, genP95: 1 }, primaryStat: "requests", offline: false }); + assert.match(svg, /ALL MODELS/); +});