135 lines
3.9 KiB
TypeScript
135 lines
3.9 KiB
TypeScript
import type { PerfPoint } from "@/lib/results";
|
|
|
|
/**
|
|
* Aggregate throughput plotted against concurrency, drawn as a scope sweep.
|
|
*
|
|
* The x axis is logarithmic because concurrency is sampled in octaves
|
|
* (1, 8, 32). On a linear axis the low-concurrency points — the ones that
|
|
* describe what a single user actually feels — collapse against the origin
|
|
* and the most important part of the curve becomes unreadable.
|
|
*/
|
|
export function ScopeTrace({
|
|
points,
|
|
width = 520,
|
|
height = 150,
|
|
showAxis = true,
|
|
}: {
|
|
points: PerfPoint[];
|
|
width?: number;
|
|
height?: number;
|
|
showAxis?: boolean;
|
|
}) {
|
|
if (points.length < 2) return null;
|
|
|
|
const pad = { top: 14, right: 14, bottom: showAxis ? 22 : 10, left: showAxis ? 46 : 10 };
|
|
const w = width - pad.left - pad.right;
|
|
const h = height - pad.top - pad.bottom;
|
|
|
|
const xs = points.map((p) => Math.log2(Math.max(1, p.concurrency)));
|
|
const ys = points.map((p) => p.output_tps_total);
|
|
const xMax = Math.max(...xs) || 1;
|
|
const yMax = Math.max(...ys) * 1.15;
|
|
|
|
const px = (i: number) => pad.left + (xs[i] / xMax) * w;
|
|
const py = (v: number) => pad.top + h - (v / yMax) * h;
|
|
|
|
const path = points.map((p, i) => `${i === 0 ? "M" : "L"}${px(i)},${py(p.output_tps_total)}`).join(" ");
|
|
const area = `${path} L${px(points.length - 1)},${pad.top + h} L${px(0)},${pad.top + h} Z`;
|
|
|
|
// Rough path length so the draw-on animation has a dash offset to work with.
|
|
const traceLen = Math.round(w * 1.6);
|
|
|
|
const gridLines = [0, 0.25, 0.5, 0.75, 1];
|
|
|
|
return (
|
|
<svg
|
|
viewBox={`0 0 ${width} ${height}`}
|
|
className="w-full"
|
|
role="img"
|
|
aria-label="Aggregate throughput against concurrency"
|
|
>
|
|
<defs>
|
|
<linearGradient id="trace-fill" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="0%" stopColor="var(--color-signal)" stopOpacity="0.22" />
|
|
<stop offset="100%" stopColor="var(--color-signal)" stopOpacity="0" />
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
{gridLines.map((g) => (
|
|
<line
|
|
key={g}
|
|
x1={pad.left}
|
|
x2={pad.left + w}
|
|
y1={pad.top + h - g * h}
|
|
y2={pad.top + h - g * h}
|
|
stroke="var(--color-rule)"
|
|
strokeWidth="1"
|
|
/>
|
|
))}
|
|
|
|
{showAxis &&
|
|
gridLines
|
|
.filter((g) => g > 0)
|
|
.map((g) => (
|
|
<text
|
|
key={`l${g}`}
|
|
x={pad.left - 8}
|
|
y={pad.top + h - g * h + 3}
|
|
textAnchor="end"
|
|
className="fill-[var(--color-dimmer)]"
|
|
style={{ fontSize: 9, fontFamily: "var(--font-mono)" }}
|
|
>
|
|
{Math.round(yMax * g)}
|
|
</text>
|
|
))}
|
|
|
|
<path d={area} fill="url(#trace-fill)" />
|
|
<path
|
|
d={path}
|
|
fill="none"
|
|
stroke="var(--color-signal)"
|
|
strokeWidth="1.75"
|
|
strokeLinejoin="round"
|
|
strokeLinecap="round"
|
|
className="trace"
|
|
style={{ ["--trace-len" as string]: traceLen }}
|
|
/>
|
|
|
|
{points.map((p, i) => (
|
|
<g key={p.concurrency}>
|
|
<circle
|
|
cx={px(i)}
|
|
cy={py(p.output_tps_total)}
|
|
r="3"
|
|
fill="var(--color-void)"
|
|
stroke="var(--color-signal)"
|
|
strokeWidth="1.5"
|
|
/>
|
|
{showAxis && (
|
|
<text
|
|
x={px(i)}
|
|
y={height - 6}
|
|
textAnchor="middle"
|
|
className="fill-[var(--color-dimmer)]"
|
|
style={{ fontSize: 9, fontFamily: "var(--font-mono)" }}
|
|
>
|
|
{p.concurrency}
|
|
</text>
|
|
)}
|
|
</g>
|
|
))}
|
|
|
|
{showAxis && (
|
|
<text
|
|
x={pad.left}
|
|
y={11}
|
|
className="fill-[var(--color-dimmer)]"
|
|
style={{ fontSize: 9, letterSpacing: "0.14em", fontFamily: "var(--font-mono)" }}
|
|
>
|
|
TOK/S AGGREGATE ↑ / CONCURRENCY →
|
|
</text>
|
|
)}
|
|
</svg>
|
|
);
|
|
}
|