Show in-flight request count + activity spark on In-Flight Monitor key

The key now renders the live request count (large) when a model is active,
plus a 1Hz count-trend sparkline of the last 60s in a bottom strip. IDLE/
LOADING/OFF/OFFLINE states unchanged. History lives in per-key state with
a sampler cleared on disappear.
This commit is contained in:
2026-08-14 11:20:51 -06:00
parent 5a6ad54b08
commit a805195c49
3 changed files with 75 additions and 8 deletions
+17 -1
View File
@@ -21,8 +21,12 @@ type InflightState = {
unsubscribe?: () => void; unsubscribe?: () => void;
pulseTimer?: ReturnType<typeof setInterval>; pulseTimer?: ReturnType<typeof setInterval>;
pulse: boolean; pulse: boolean;
history: number[];
sampler?: ReturnType<typeof setInterval>;
}; };
const HISTORY_LIMIT = 60;
@action({ UUID: "com.bryce.llamawatch.inflight" }) @action({ UUID: "com.bryce.llamawatch.inflight" })
export class InflightMonitor extends SingletonAction<InflightSettings> { export class InflightMonitor extends SingletonAction<InflightSettings> {
private states = new Map<string, InflightState>(); private states = new Map<string, InflightState>();
@@ -30,7 +34,7 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
private stateFor(action: KeyAction<InflightSettings>): InflightState { private stateFor(action: KeyAction<InflightSettings>): InflightState {
let state = this.states.get(action.id); let state = this.states.get(action.id);
if (!state) { if (!state) {
state = { settings: {}, pulse: false }; state = { settings: {}, pulse: false, history: [] };
this.states.set(action.id, state); this.states.set(action.id, state);
} }
return state; return state;
@@ -43,6 +47,7 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
state.action = ev.action; state.action = ev.action;
runtime.ensureConnections(cfgFromSettings(state.settings)); runtime.ensureConnections(cfgFromSettings(state.settings));
state.unsubscribe = runtime.subscribe(() => this.render(state)); state.unsubscribe = runtime.subscribe(() => this.render(state));
state.sampler = setInterval(() => this.sample(state), 1000);
this.render(state); this.render(state);
} }
@@ -50,6 +55,8 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
const state = this.states.get(ev.action.id); const state = this.states.get(ev.action.id);
if (!state) return; if (!state) return;
state.unsubscribe?.(); state.unsubscribe?.();
if (state.sampler) clearInterval(state.sampler);
state.sampler = undefined;
this.clearPulse(state); this.clearPulse(state);
this.states.delete(ev.action.id); this.states.delete(ev.action.id);
} }
@@ -59,6 +66,7 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
const state = this.stateFor(ev.action); const state = this.stateFor(ev.action);
state.settings = ev.payload.settings; state.settings = ev.payload.settings;
state.action = ev.action; state.action = ev.action;
state.history = [];
runtime.ensureConnections(cfgFromSettings(state.settings)); runtime.ensureConnections(cfgFromSettings(state.settings));
this.render(state); this.render(state);
} }
@@ -82,6 +90,7 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
count, count,
offline, offline,
pulse: state.pulse, pulse: state.pulse,
history: state.history,
}), }),
), ),
); );
@@ -101,4 +110,11 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
state.pulseTimer = undefined; state.pulseTimer = undefined;
state.pulse = false; state.pulse = false;
} }
private sample(state: InflightState): void {
const modelId = state.settings.modelId ?? "";
state.history.push(runtime.tracker.count(modelId));
if (state.history.length > HISTORY_LIMIT) state.history.shift();
this.render(state);
}
} }
+35 -5
View File
@@ -20,6 +20,7 @@ export interface InflightRenderOptions {
count: number; count: number;
offline: boolean; offline: boolean;
pulse: boolean; pulse: boolean;
history?: number[];
} }
export function renderInflight(opts: InflightRenderOptions): string { export function renderInflight(opts: InflightRenderOptions): string {
@@ -46,13 +47,26 @@ export function renderInflight(opts: InflightRenderOptions): string {
const isActive = opts.state === "ready" && opts.count > 0; const isActive = opts.state === "ready" && opts.count > 0;
const bg = opts.state === "ready" ? (isActive ? "#8b2626" : "#1e6b34") : opts.state === "loading" ? "#8a6d1d" : "#3a3a3a"; const bg = opts.state === "ready" ? (isActive ? "#8b2626" : "#1e6b34") : opts.state === "loading" ? "#8a6d1d" : "#3a3a3a";
const label = opts.state === "ready" ? (isActive ? "ACTIVE" : "IDLE") : opts.state === "loading" ? "LOADING" : "OFF"; const center = isActive ? `${opts.count}` : opts.state === "ready" ? "IDLE" : opts.state === "loading" ? "LOADING" : "OFF";
const centerSize = isActive ? 24 : 16;
const opacity = isActive && opts.pulse ? 0.55 : 1; const opacity = isActive && opts.pulse ? 0.55 : 1;
return frame(bg, [ const parts: string[] = [
centerText(label, 38, 18, "bold", "#ffffff", opacity), centerText(name, 12, 7, "normal", "#ffffff"),
centerText(name, 64, 7, "normal", "#ffffff"), centerText(center, 40, centerSize, "bold", "#ffffff", opacity),
]); ];
if (opts.state === "ready") {
const points = inflightSparkPoints(opts.history ?? []);
if (points.length > 1) {
parts.push(
`<path d="M ${points.map((p) => `${p.x},${p.y}`).join(" L ")} L ${points[points.length - 1].x},63 L ${points[0].x},63 Z" fill="#ffffff" opacity="0.18"/>`,
`<polyline points="${points.map((p) => `${p.x},${p.y}`).join(" ")}" fill="none" stroke="#ffffff" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" opacity="0.8"/>`,
);
}
}
return frame(bg, parts);
} }
export interface GpuGraphRenderOptions { export interface GpuGraphRenderOptions {
@@ -132,6 +146,22 @@ function chartPoints(history: number[], metric: GpuMetricKind): { x: number; y:
}); });
} }
function inflightSparkPoints(history: number[]): { x: number; y: number }[] {
const n = history.length;
if (n === 0) return [];
const left = 4;
const right = 68;
const top = 50;
const bottom = 63;
const yMax = Math.max(...history, 1);
return history.map((v, i) => {
const x = n === 1 ? (left + right) / 2 : left + ((right - left) * i) / (n - 1);
const clamped = Math.max(0, Math.min(v, yMax));
const y = bottom - (clamped / yMax) * (bottom - top);
return { x: round1(x), y: round1(y) };
});
}
function yScaleMax(metric: GpuMetricKind, history: number[]): number { function yScaleMax(metric: GpuMetricKind, history: number[]): number {
if (metric !== "power") return 100; if (metric !== "power") return 100;
const maxValue = Math.max(...history); const maxValue = Math.max(...history);
+23 -2
View File
@@ -14,10 +14,31 @@ test("renderInflight: ready + count renders IDLE in green", () => {
assert.doesNotMatch(svg, /ACTIVE/); assert.doesNotMatch(svg, /ACTIVE/);
}); });
test("renderInflight: ready + count>0 renders ACTIVE in red", () => { test("renderInflight: ready + count>0 renders the count number on red", () => {
const svg = renderInflight({ modelName: "Qwen3.8-27B-NVFP4", state: "ready", count: 2, offline: false, pulse: false }); const svg = renderInflight({ modelName: "Qwen3.8-27B-NVFP4", state: "ready", count: 2, offline: false, pulse: false });
assert.match(svg, /ACTIVE/); assert.match(svg, /2/);
assert.match(svg, /#8b2626/); assert.match(svg, /#8b2626/);
assert.doesNotMatch(svg, /ACTIVE/);
assert.doesNotMatch(svg, /IDLE/);
});
test("renderInflight: idle state does not show a count number", () => {
const svg = renderInflight({ modelName: "A", state: "ready", count: 0, offline: false, pulse: false });
assert.match(svg, /IDLE/);
assert.doesNotMatch(svg, /font-size="24"/);
});
test("renderInflight: draws a count-trend spark when history has 2+ samples", () => {
const svg = renderInflight({ modelName: "A", state: "ready", count: 1, offline: false, pulse: false, history: [0, 1, 2, 1] });
assert.match(svg, /<polyline/);
assert.match(svg, /<path/);
});
test("renderInflight: no spark with fewer than 2 history samples", () => {
const svg = renderInflight({ modelName: "A", state: "ready", count: 1, offline: false, pulse: false, history: [1] });
assert.doesNotMatch(svg, /<polyline/);
const none = renderInflight({ modelName: "A", state: "ready", count: 1, offline: false, pulse: false });
assert.doesNotMatch(none, /<polyline/);
}); });
test("renderInflight: pulse toggles opacity while active", () => { test("renderInflight: pulse toggles opacity while active", () => {