Profiling page: per-turn phase timings, live in the web dashboard

The engine already tracks where each turn's wall time goes (expert-disk
service, I/O wait, expert matmul, attention, lm_head) — it just only spoke
at exit or under PROF=1. Stream it instead:

- glm.c: mux serve emits a per-turn "PROF" protocol line next to TIERS/HITS
  (window deltas per request, same convention as the STAT hit%); the phase
  window base is now always captured (a few loads per request).
- openai_server.py: parses PROF into a 120-turn rolling window and serves it
  at /profile (read-only, same trust level as /health).
- web: new Profiling tab — stat tiles (tok/s, wall, tokens/forward, disk
  service), wall-time composition bars for the last turn and the window,
  per-turn throughput and stacked phase columns with hover readouts, and a
  table of recent turns. Disk service is shown apart from the stack: it
  overlaps with compute, so only the I/O wait the compute thread felt counts
  inside wall time. Phase colours are a CVD-validated set with gaps + legend
  + table so identity never rides on colour alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WhTmF8yvEBgSkUKSVfZF7P
This commit is contained in:
Claude
2026-07-14 23:48:54 +00:00
committed by Nicholas Beerbower
parent 63a6824881
commit 6afffbcbf2
10 changed files with 321 additions and 8 deletions
+3 -2
View File
@@ -540,9 +540,10 @@ What you get:
- **Chat** with live metrics: a flashing token counter while generating, then tok/s, time-to-first-token, prompt→completion counts and queue wait;
- **Runtime panel**: your hardware (CPU, GPUs + VRAM, RAM, cores), the scheduler, and the live expert-tier bar — how many of the 19,456 experts sit in VRAM / RAM / disk right now;
- **Brain**: the whole model as a 76×256 cortex, one cell per expert. Colour = tier, brightness = routing heat, and the experts routed in each turn flash white and decay — you watch the model think. Hover any cell for its tier, heat and [measured topic affinity](https://github.com/JustVugg/colibri/issues/175) (specialists for code, Chinese, math, law… live in layers 1122).
- **Brain**: the whole model as a 76×256 cortex, one cell per expert. Colour = tier, brightness = routing heat, and the experts routed in each turn flash white and decay — you watch the model think. Hover any cell for its tier, heat and [measured topic affinity](https://github.com/JustVugg/colibri/issues/175) (specialists for code, Chinese, math, law… live in layers 1122);
- **Profiling**: where each turn's wall time went — I/O wait vs expert matmul vs attention vs LM head — as stacked per-turn bars, plus throughput history, tokens-per-forward batching, and a table of the recent turns. The same phase timers behind the `PROFILE` line, streamed live.
The dashboard talks to the engine over two tiny protocol lines (`TIERS`, `EMAP`/`HITS`) and plain JSON endpoints — nothing heavier than the engine itself.
The dashboard talks to the engine over a few tiny protocol lines (`TIERS`, `EMAP`/`HITS`, `PROF`) and plain JSON endpoints — nothing heavier than the engine itself.
## Got a better machine? Try it — here's what to expect
+13 -2
View File
@@ -4874,7 +4874,8 @@ typedef struct {
float temp, top_p;
double started;
uint64_t hits0, miss0;
ProfBase pb; /* PROF=1: window start (same convention as hits0) */
ProfBase pb; /* phase-time window start (same convention as hits0):
feeds the PROF protocol line and the PROF=1 report */
} ServeReq;
static void mux_data(Tok *T, unsigned long long id, int token){
@@ -4891,6 +4892,16 @@ static void mux_done(Model *m, ServeCtx *sc, ServeReq *r){
tiers_emit(m);
emap_emit(m);
hits_emit(m);
/* PROF: per-turn phase timings for the dashboard profiling page —
* "PROF <wall_s> <prompt> <completion> <edisk> <ewait> <emm> <attn> <head> <n_fw>".
* With KV_SLOTS>1 concurrent slots share the batched forwards, so the shares
* describe the whole engine over the window, not the single request (same
* convention as the STAT hit% below). */
printf("PROF %.3f %d %d %.3f %.3f %.3f %.3f %.3f %llu\n",dt,
r->prompt_tokens,r->emitted,
m->t_edisk-r->pb.edisk,m->t_ewait-r->pb.ewait,m->t_emm-r->pb.emm,
m->t_attn-r->pb.attn,m->t_head-r->pb.head,
(unsigned long long)(m->n_fw-r->pb.n_fw));
printf("DONE %llu STAT %d %.2f %.1f %.2f %d %d\n",r->id,r->emitted,
r->emitted/dt,(dh+dm)>0?100.0*dh/(dh+dm):0.0,rss_gb(),
r->prompt_tokens,r->length_limited);
@@ -4961,7 +4972,7 @@ static int mux_submit(Model *m, Tok *T, ServeCtx *ctx, ServeReq *req, int nctx,
ServeReq *r=&req[sub.slot]; memset(r,0,sizeof(*r));
r->id=sub.id; r->maximum=sub.max_tokens; r->temp=sub.temperature; r->top_p=sub.top_p;
r->prompt_tokens=nt; r->started=now_s(); r->hits0=m->hits; r->miss0=m->miss;
if(g_prof) prof_base(m,&r->pb);
prof_base(m,&r->pb); /* a few loads: cheap enough to always track */
int room=maxctx-sc->len-1; if(r->maximum>room){r->maximum=room; r->length_limited=1;}
g_temp=r->temp; g_nuc=r->top_p;
int next=pick_tok(logit,m->c.vocab,-1); free(logit);
+23
View File
@@ -27,6 +27,7 @@ HERE = Path(__file__).resolve().parent
END = b"\x01\x01END\x01\x01\n"
READY = b"\x01\x01READY\x01\x01\n"
MAX_BODY = 4 << 20
PROFILE_TURNS = 120 # rolling window of per-turn PROF snapshots kept for /profile
DEFAULT_CORS_ORIGINS = (
"http://127.0.0.1:8000",
"http://localhost:8000",
@@ -473,6 +474,8 @@ class Engine:
self.emap = None
self.hits = None
self.hits_seq = 0 # latest "TIERS" snapshot from the engine
self.profile = collections.deque(maxlen=PROFILE_TURNS) # per-turn phase timings
self.profile_seq = 0
read_engine_turn(self.process.stdout, READY, lambda _: None)
self.dispatcher = threading.Thread(target=self._dispatch_stdout,
name="colibri-stdout", daemon=True)
@@ -550,6 +553,20 @@ class Engine:
elif kind == "HITS" and len(fields) == 4:
self.hits = fields[3]
self.hits_seq += 1
elif kind == "PROF" and len(fields) >= 10:
# per-turn phase timings: where the engine spent this turn's wall time
self.profile.append({
"wall_s": float(fields[1]),
"prompt_tokens": int(fields[2]),
"completion_tokens": int(fields[3]),
"expert_disk_s": float(fields[4]),
"expert_wait_s": float(fields[5]),
"expert_matmul_s": float(fields[6]),
"attention_s": float(fields[7]),
"lm_head_s": float(fields[8]),
"forwards": int(fields[9]),
})
self.profile_seq += 1
elif kind == "TIERS" and len(fields) >= 6:
self.tiers = {"vram": int(fields[1]), "ram": int(fields[2]),
"disk": int(fields[3]), "vram_gb": float(fields[4]),
@@ -782,6 +799,12 @@ class APIHandler(BaseHTTPRequestHandler):
payload["seq"] = eng.hits_seq
self.send_json(200, payload, request_id)
return
if path == "/profile":
eng = self.server.engine
payload = {"seq": getattr(eng, "profile_seq", 0) if eng else 0,
"turns": list(getattr(eng, "profile", ()) or ()) if eng else []}
self.send_json(200, payload, request_id)
return
if self.serve_static(path):
return
self.require_auth()
+33
View File
@@ -380,6 +380,25 @@ class DispatcherTest(unittest.TestCase):
engine.close()
self.assertEqual(chunks, ["é"])
def test_records_profile_snapshots_from_prof_lines(self):
def respond(process, frame):
request_id = frame.split()[1]
process.stdout.feed(b"DATA " + request_id + b" 2\nok\n")
process.stdout.feed(b"PROF 2.500 7 12 0.400 0.100 0.900 0.600 0.200 15\n")
process.stdout.feed(b"DONE " + request_id + b" STAT 12 4.8 0 1.0 7 0\n")
process = FakeProcess(respond)
with patch("openai_server.subprocess.Popen", return_value=process):
engine = Engine("glm", "model")
engine.generate("hello", 16, 0.7, 0.9, lambda _: None)
engine.close()
self.assertEqual(engine.profile_seq, 1)
self.assertEqual(list(engine.profile), [{
"wall_s": 2.5, "prompt_tokens": 7, "completion_tokens": 12,
"expert_disk_s": 0.4, "expert_wait_s": 0.1, "expert_matmul_s": 0.9,
"attention_s": 0.6, "lm_head_s": 0.2, "forwards": 15,
}])
def test_cancels_generation_after_consumer_disconnects(self):
request_id = None
@@ -443,6 +462,20 @@ class HTTPTest(unittest.TestCase):
self.assertIn("queued", scheduler)
self.assertEqual(health["kv_slots"], 2)
def test_profile_reports_recent_turns_without_auth(self):
with urlopen(self.base + "/profile", timeout=2) as response:
self.assertEqual(json.load(response), {"seq": 0, "turns": []})
turn = {"wall_s": 2.5, "prompt_tokens": 7, "completion_tokens": 12,
"expert_disk_s": 0.4, "expert_wait_s": 0.1, "expert_matmul_s": 0.9,
"attention_s": 0.6, "lm_head_s": 0.2, "forwards": 15}
self.engine.profile = [turn]
self.engine.profile_seq = 1
try:
with urlopen(self.base + "/profile", timeout=2) as response:
self.assertEqual(json.load(response), {"seq": 1, "turns": [turn]})
finally:
del self.engine.profile, self.engine.profile_seq
def test_browser_preflight(self):
request = Request(self.base + "/v1/chat/completions", method="OPTIONS", headers={
"Origin": "http://localhost:5173",
+6 -1
View File
@@ -17,9 +17,14 @@ npm test
npm run build
```
Besides Chat and Brain, the **Profiling** tab charts where the engine spent
each turn's wall time (I/O wait, expert matmul, attention, LM head) from the
server's `/profile` endpoint — a rolling window of per-turn `PROF` snapshots
emitted by the engine.
The test suite stays browser-light: API requests use a mocked `fetch`, while
runtime capability and storage behavior are covered through pure helpers. It
checks that `/health` is resolved next to (not below) the OpenAI `/v1` prefix,
checks that `/health` and `/profile` are resolved next to (not below) the OpenAI `/v1` prefix,
supports both boolean and numeric `scheduler.active` responses, and sends the
colibrì-specific `cache_slot` field only when KV-slot support was advertised.
+5 -2
View File
@@ -31,6 +31,7 @@ import { Textarea } from "@/components/ui/textarea"
import { getHealth, listModels, streamChat, type ChatMessage, type HealthResponse, type StreamChatResult } from "@/lib/api"
import { activeRequests, supportsCacheSlots } from "@/lib/runtime"
import { Brain } from "./Brain"
import { Profiling } from "./Profiling"
import { persistPublicSettings, stored } from "@/lib/storage"
import { cn } from "@/lib/utils"
@@ -73,7 +74,7 @@ export default function App() {
const [totalTokens, setTotalTokens] = useState({ prompt: 0, completion: 0 })
const [connecting, setConnecting] = useState(false)
const [connected, setConnected] = useState(false)
const [view, setView] = useState<"chat" | "brain">("chat")
const [view, setView] = useState<"chat" | "brain" | "profiling">("chat")
const [error, setError] = useState("")
const autoConnected = useRef(false)
const abortRef = useRef<AbortController | null>(null)
@@ -314,6 +315,7 @@ export default function App() {
<div className="view-tabs">
<button className={view === "chat" ? "active" : ""} onClick={() => setView("chat")}><MessageSquareText className="size-3.5" /> Chat</button>
<button className={view === "brain" ? "active" : ""} onClick={() => setView("brain")}><BrainCircuit className="size-3.5" /> Brain</button>
<button className={view === "profiling" ? "active" : ""} onClick={() => setView("profiling")}><Gauge className="size-3.5" /> Profiling</button>
</div>
<div className="top-actions">
{loading && tokenCount > 0 ? <Badge className="badge-live"><Zap className="size-3 flash" /> {tokenCount} tokens</Badge> : null}
@@ -326,7 +328,8 @@ export default function App() {
</div>
</header>
{view === "brain" ? <Brain baseUrl={baseUrl} apiKey={apiKey} connected={connected} /> : <>
{view === "brain" ? <Brain baseUrl={baseUrl} apiKey={apiKey} connected={connected} />
: view === "profiling" ? <Profiling baseUrl={baseUrl} apiKey={apiKey} connected={connected} /> : <>
<div className="conversation">
{!messages.length ? (
+165
View File
@@ -0,0 +1,165 @@
import { useEffect, useState } from "react"
import { Activity, Gauge, HardDrive, Timer } from "lucide-react"
import { getProfile, type ProfileTurn } from "@/lib/api"
/* Wall-time phases stacked per turn. The order is the palette's CVD-safe slot
* order (validated as a set on this surface) — identity never leans on colour
* alone: segments keep 2px gaps, the legend is always shown and the table
* carries the exact numbers. Disk *service* time is reported separately: it
* runs on I/O threads overlapped with compute, so only the stall the compute
* thread actually felt (I/O wait) belongs inside the wall-time stack. */
const PHASES = [
{ key: "expert_wait_s", name: "I/O wait", color: "#3987e5" },
{ key: "expert_matmul_s", name: "Expert matmul", color: "#199e70" },
{ key: "attention_s", name: "Attention", color: "#c98500" },
{ key: "lm_head_s", name: "LM head", color: "#008300" },
{ key: "other_s", name: "Other", color: "#9085e9" },
] as const
interface Turn extends ProfileTurn { other_s: number; toks: number }
const derive = (turn: ProfileTurn): Turn => ({
...turn,
other_s: Math.max(0, turn.wall_s - turn.expert_wait_s - turn.expert_matmul_s - turn.attention_s - turn.lm_head_s),
toks: turn.wall_s > 0 ? turn.completion_tokens / turn.wall_s : 0,
})
const seconds = (value: number) => (value >= 10 ? value.toFixed(1) : value.toFixed(2)) + "s"
function ShareBar({ label, turns }: { label: string; turns: Turn[] }) {
const total = turns.reduce((sum, turn) => sum + turn.wall_s, 0)
const parts = PHASES.map((phase) => ({ ...phase, value: turns.reduce((sum, turn) => sum + turn[phase.key], 0) }))
return (
<div className="prof-share">
<div className="prof-share-head"><span>{label}</span><code>{seconds(total)}</code></div>
<div className="prof-share-bar" role="img" aria-label={parts.map((part) => `${part.name} ${seconds(part.value)}`).join(", ")}>
{parts.map((part) => {
const share = total > 0 ? part.value / total : 0
return share > 0.001 ? (
<span key={part.key} style={{ width: `${100 * share}%`, background: part.color }} title={`${part.name}${seconds(part.value)} (${(100 * share).toFixed(1)}%)`}>
{share >= 0.09 ? `${Math.round(100 * share)}%` : ""}
</span>
) : null
})}
</div>
</div>
)
}
/* Column chart over the recent turns; oldest on the left. Stacked mode draws the
* wall-time composition, plain mode a single series (no legend — the title names it). */
function TurnColumns({ turns, stacked, height, format }: { turns: Turn[]; stacked: boolean; height: number; format: (turn: Turn) => string }) {
const [hover, setHover] = useState<number | null>(null)
const peak = Math.max(...turns.map((turn) => (stacked ? turn.wall_s : turn.toks)), 1e-9)
const gap = 2
const width = Math.max(1, (100 - gap * (turns.length - 1)) / turns.length)
return (
<div className="prof-plot" onMouseLeave={() => setHover(null)}>
<svg viewBox={`0 0 100 ${height}`} preserveAspectRatio="none" aria-hidden="true">
{[0.25, 0.5, 0.75].map((line) => <line key={line} x1="0" x2="100" y1={height * line} y2={height * line} className="prof-grid" />)}
{turns.map((turn, index) => {
const x = index * (width + gap)
if (!stacked) {
const h = (height * turn.toks) / peak
return <rect key={index} x={x} y={height - h} width={width} height={h} rx="1" fill="var(--primary)" opacity={hover === null || hover === index ? 1 : 0.45} />
}
let y = height
return PHASES.map((phase) => {
const h = (height * turn[phase.key]) / peak
y -= h
return h > 0.1 ? <rect key={`${index}-${phase.key}`} x={x} y={y + 0.35} width={width} height={Math.max(h - 0.7, 0.35)} fill={phase.color} opacity={hover === null || hover === index ? 1 : 0.45} /> : null
})
})}
{/* hit targets bigger than the marks */}
{turns.map((_, index) => <rect key={index} x={index * (width + gap) - gap / 2} y="0" width={width + gap} height={height} fill="transparent" onMouseEnter={() => setHover(index)} />)}
</svg>
<div className="prof-plot-foot">
<span>{turns.length > 1 ? `${turns.length} turns · oldest → newest` : "1 turn"}</span>
<code>{hover !== null && turns[hover] ? format(turns[hover]) : `peak ${stacked ? seconds(peak) : peak.toFixed(1) + " tok/s"}`}</code>
</div>
</div>
)
}
export function Profiling({ baseUrl, apiKey, connected }: { baseUrl: string; apiKey: string; connected: boolean }) {
const [turns, setTurns] = useState<Turn[]>([])
useEffect(() => {
if (!connected) return
let disposed = false
const poll = async () => {
if (document.visibilityState === "hidden") return
try {
const result = await getProfile(baseUrl, apiKey)
if (!disposed) setTurns(result.turns.map(derive))
} catch { /* engine busy or restarting — keep the last snapshot */ }
}
void poll()
const timer = window.setInterval(() => void poll(), 2000)
return () => { disposed = true; window.clearInterval(timer) }
}, [baseUrl, apiKey, connected])
const latest = turns[turns.length - 1]
const recent = turns.slice(-40)
const diskService = turns.reduce((sum, turn) => sum + turn.expert_disk_s, 0)
return (
<div className="prof-page">
<div className="prof-head">
<div className="section-title"><Gauge className="size-4" /> Profiling where the engine spends each turn</div>
<div className="prof-legend">
{PHASES.map((phase) => <span key={phase.key}><i style={{ background: phase.color }} />{phase.name}</span>)}
</div>
</div>
{!latest ? (
<p className="runtime-unavailable">{connected ? "No profiled turns yet — send a chat message and the breakdown appears here." : "Connect to the engine to collect per-turn timings."}</p>
) : (
<>
<div className="prof-tiles">
<div><span><Gauge className="size-3" /> Last turn</span><strong>{latest.toks.toFixed(1)}</strong><small>tok/s</small></div>
<div><span><Timer className="size-3" /> Wall time</span><strong>{seconds(latest.wall_s)}</strong><small>{latest.prompt_tokens} {latest.completion_tokens} tokens</small></div>
<div><span><Activity className="size-3" /> Batching</span><strong>{latest.forwards > 0 ? (latest.completion_tokens / latest.forwards).toFixed(2) : "—"}</strong><small>tokens / forward</small></div>
<div><span><HardDrive className="size-3" /> Disk service</span><strong>{seconds(latest.expert_disk_s)}</strong><small>overlapped with compute</small></div>
</div>
<div className="prof-shares">
<ShareBar label="Last turn" turns={[latest]} />
{turns.length > 1 ? <ShareBar label={`Window · last ${turns.length} turns`} turns={turns} /> : null}
</div>
<div className="prof-charts">
<div className="prof-chart">
<div className="prof-chart-title">Throughput per turn (tok/s)</div>
<TurnColumns turns={recent} stacked={false} height={36} format={(turn) => `${turn.toks.toFixed(1)} tok/s · ${turn.completion_tokens} tokens`} />
</div>
<div className="prof-chart">
<div className="prof-chart-title">Turn wall time by phase (s)</div>
<TurnColumns turns={recent} stacked height={36} format={(turn) => `${seconds(turn.wall_s)} · ${PHASES.map((phase) => `${phase.name} ${seconds(turn[phase.key])}`).join(" · ")}`} />
</div>
</div>
<div className="prof-table-wrap">
<table className="prof-table">
<thead><tr><th>Turn</th><th>Tokens</th><th>tok/s</th><th>Wall</th>{PHASES.map((phase) => <th key={phase.key}><i style={{ background: phase.color }} />{phase.name}</th>)}<th>Disk service</th></tr></thead>
<tbody>
{recent.slice().reverse().map((turn, index) => (
<tr key={turns.length - index}>
<td>{turns.length - index}</td>
<td>{turn.prompt_tokens} {turn.completion_tokens}</td>
<td>{turn.toks.toFixed(1)}</td>
<td>{seconds(turn.wall_s)}</td>
{PHASES.map((phase) => <td key={phase.key}>{seconds(turn[phase.key])}</td>)}
<td>{seconds(turn.expert_disk_s)}</td>
</tr>
))}
</tbody>
</table>
{diskService > 0 ? <p className="prof-note">Disk service is time spent reading experts on I/O threads; it overlaps with compute, so only the <em>I/O wait</em> the compute thread felt counts inside the wall-time stack. With multiple KV sessions the shares describe the whole engine over the turn's window.</p> : null}
</div>
</>
)}
</div>
)
}
+36
View File
@@ -151,3 +151,39 @@ button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-
.brain-tip-spec { color: var(--primary); font-weight: 700; }
.brain-tip-spec small, .brain-tip-aff { color: #8b9aa3; font-weight: 400; }
.brain-tip-aff { font-size: 10px; }
/* ---- Profiling page ---- */
.prof-page { flex: 1; display: flex; flex-direction: column; gap: 16px; padding: 18px 22px; overflow: auto; }
.prof-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px; }
.prof-legend { display: flex; flex-wrap: wrap; gap: 12px; font-size: 11px; color: #a5b0b4; align-items: center; }
.prof-legend i { width: 9px; height: 9px; border-radius: 2px; display: inline-block; margin-right: 5px; vertical-align: -1px; }
.prof-tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; }
.prof-tiles > div { display: grid; gap: 4px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 11px; background: var(--card); }
.prof-tiles span { display: flex; align-items: center; gap: 5px; color: #7f8d92; font-size: 9px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
.prof-tiles strong { font: 600 22px ui-monospace, SFMono-Regular, monospace; font-variant-numeric: tabular-nums; }
.prof-tiles small { color: var(--muted-foreground); font-size: 10px; }
.prof-shares { display: grid; gap: 10px; padding: 14px; border: 1px solid var(--border); border-radius: 11px; background: var(--card); }
.prof-share { display: grid; gap: 6px; }
.prof-share-head { display: flex; justify-content: space-between; color: #a5b0b4; font-size: 11px; font-weight: 600; }
.prof-share-head code { color: var(--foreground); font-size: 11px; font-variant-numeric: tabular-nums; }
.prof-share-bar { display: flex; gap: 2px; height: 22px; border-radius: 6px; overflow: hidden; }
.prof-share-bar span { display: grid; place-items: center; min-width: 0; overflow: hidden; color: #06090b; font-size: 10px; font-weight: 700; font-variant-numeric: tabular-nums; transition: width .5s ease; }
.prof-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.prof-chart { display: grid; gap: 8px; padding: 12px 14px; border: 1px solid var(--border); border-radius: 11px; background: var(--card); }
.prof-chart-title { color: #a5b0b4; font-size: 11px; font-weight: 600; }
.prof-plot svg { display: block; width: 100%; height: 120px; }
.prof-grid { stroke: var(--border); stroke-width: .3; }
.prof-plot-foot { display: flex; justify-content: space-between; gap: 8px; margin-top: 6px; color: #7f8d92; font-size: 10px; }
.prof-plot-foot code { color: var(--foreground); font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.prof-table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: 11px; background: var(--card); }
.prof-table { width: 100%; border-collapse: collapse; font-size: 11px; font-variant-numeric: tabular-nums; }
.prof-table th { padding: 9px 12px; border-bottom: 1px solid var(--border); color: #7f8d92; font-size: 9px; font-weight: 700; letter-spacing: .07em; text-transform: uppercase; text-align: left; white-space: nowrap; }
.prof-table th i { width: 8px; height: 8px; border-radius: 2px; display: inline-block; margin-right: 5px; vertical-align: -1px; }
.prof-table td { padding: 7px 12px; border-bottom: 1px solid var(--border); color: #c3ccd0; white-space: nowrap; }
.prof-table tbody tr:last-child td { border-bottom: 0; }
.prof-note { margin: 0; padding: 10px 12px; color: var(--muted-foreground); font-size: 10px; line-height: 1.5; border-top: 1px solid var(--border); }
@media (max-width: 900px) {
.prof-page { padding: 10px; }
.prof-tiles { grid-template-columns: 1fr 1fr; }
.prof-charts { grid-template-columns: 1fr; }
}
+14 -1
View File
@@ -1,6 +1,6 @@
import { afterEach, describe, expect, it, vi } from "vitest"
import { extractSSE, getHealth, serverEndpoint, streamChat } from "./api"
import { extractSSE, getHealth, getProfile, serverEndpoint, streamChat } from "./api"
afterEach(() => vi.unstubAllGlobals())
@@ -36,6 +36,19 @@ describe("runtime API", () => {
headers: expect.objectContaining({ Authorization: "Bearer secret" }),
}))
})
it("requests the profiling history next to the OpenAI v1 prefix", async () => {
const turn = {
wall_s: 2.5, prompt_tokens: 7, completion_tokens: 12,
expert_disk_s: 0.4, expert_wait_s: 0.1, expert_matmul_s: 0.9,
attention_s: 0.6, lm_head_s: 0.2, forwards: 15,
}
const fetchMock = vi.fn().mockResolvedValue(new Response(JSON.stringify({ seq: 1, turns: [turn] })))
vi.stubGlobal("fetch", fetchMock)
await expect(getProfile("http://localhost:8000/v1/")).resolves.toEqual({ seq: 1, turns: [turn] })
expect(fetchMock).toHaveBeenCalledWith("http://localhost:8000/profile", expect.anything())
})
})
describe("chat request extensions", () => {
+23
View File
@@ -49,6 +49,23 @@ export interface HealthResponse {
hwinfo?: HwinfoHealth
}
export interface ProfileTurn {
wall_s: number
prompt_tokens: number
completion_tokens: number
expert_disk_s: number
expert_wait_s: number
expert_matmul_s: number
attention_s: number
lm_head_s: number
forwards: number
}
export interface ProfileResponse {
seq: number
turns: ProfileTurn[]
}
export interface TokenUsage {
prompt_tokens: number
completion_tokens: number
@@ -100,6 +117,12 @@ export async function getHealth(baseUrl: string, apiKey = "", signal?: AbortSign
return (await response.json()) as HealthResponse
}
export async function getProfile(baseUrl: string, apiKey = "", signal?: AbortSignal): Promise<ProfileResponse> {
const response = await fetch(serverEndpoint(baseUrl, "profile"), { headers: headers(apiKey), signal })
if (!response.ok) throw new Error(await responseError(response))
return (await response.json()) as ProfileResponse
}
export function extractSSE(buffer: string) {
const frames = buffer.split(/\r?\n\r?\n/)
const rest = frames.pop() || ""