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;
|
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
@@ -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
@@ -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", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user