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:
@@ -21,8 +21,12 @@ type InflightState = {
|
||||
unsubscribe?: () => void;
|
||||
pulseTimer?: ReturnType<typeof setInterval>;
|
||||
pulse: boolean;
|
||||
history: number[];
|
||||
sampler?: ReturnType<typeof setInterval>;
|
||||
};
|
||||
|
||||
const HISTORY_LIMIT = 60;
|
||||
|
||||
@action({ UUID: "com.bryce.llamawatch.inflight" })
|
||||
export class InflightMonitor extends SingletonAction<InflightSettings> {
|
||||
private states = new Map<string, InflightState>();
|
||||
@@ -30,7 +34,7 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
|
||||
private stateFor(action: KeyAction<InflightSettings>): InflightState {
|
||||
let state = this.states.get(action.id);
|
||||
if (!state) {
|
||||
state = { settings: {}, pulse: false };
|
||||
state = { settings: {}, pulse: false, history: [] };
|
||||
this.states.set(action.id, state);
|
||||
}
|
||||
return state;
|
||||
@@ -43,6 +47,7 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
|
||||
state.action = ev.action;
|
||||
runtime.ensureConnections(cfgFromSettings(state.settings));
|
||||
state.unsubscribe = runtime.subscribe(() => this.render(state));
|
||||
state.sampler = setInterval(() => this.sample(state), 1000);
|
||||
this.render(state);
|
||||
}
|
||||
|
||||
@@ -50,6 +55,8 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
|
||||
const state = this.states.get(ev.action.id);
|
||||
if (!state) return;
|
||||
state.unsubscribe?.();
|
||||
if (state.sampler) clearInterval(state.sampler);
|
||||
state.sampler = undefined;
|
||||
this.clearPulse(state);
|
||||
this.states.delete(ev.action.id);
|
||||
}
|
||||
@@ -59,6 +66,7 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
|
||||
const state = this.stateFor(ev.action);
|
||||
state.settings = ev.payload.settings;
|
||||
state.action = ev.action;
|
||||
state.history = [];
|
||||
runtime.ensureConnections(cfgFromSettings(state.settings));
|
||||
this.render(state);
|
||||
}
|
||||
@@ -82,6 +90,7 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
|
||||
count,
|
||||
offline,
|
||||
pulse: state.pulse,
|
||||
history: state.history,
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -101,4 +110,11 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
|
||||
state.pulseTimer = undefined;
|
||||
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
@@ -20,6 +20,7 @@ export interface InflightRenderOptions {
|
||||
count: number;
|
||||
offline: boolean;
|
||||
pulse: boolean;
|
||||
history?: number[];
|
||||
}
|
||||
|
||||
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 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;
|
||||
|
||||
return frame(bg, [
|
||||
centerText(label, 38, 18, "bold", "#ffffff", opacity),
|
||||
centerText(name, 64, 7, "normal", "#ffffff"),
|
||||
]);
|
||||
const parts: string[] = [
|
||||
centerText(name, 12, 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 {
|
||||
@@ -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 {
|
||||
if (metric !== "power") return 100;
|
||||
const maxValue = Math.max(...history);
|
||||
|
||||
+23
-2
@@ -14,10 +14,31 @@ test("renderInflight: ready + count renders IDLE in green", () => {
|
||||
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 });
|
||||
assert.match(svg, /ACTIVE/);
|
||||
assert.match(svg, /2/);
|
||||
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", () => {
|
||||
|
||||
Reference in New Issue
Block a user