fix: refcount StatsCache keys, keep registrations across config change, and tidy GPU/usage render paths

This commit is contained in:
c4ch3c4d3
2026-08-14 13:04:56 -06:00
parent d04eb6b524
commit 21d2e45f9e
5 changed files with 75 additions and 48 deletions
+2 -5
View File
@@ -87,7 +87,7 @@ export class GpuGraph extends SingletonAction<GpuSettings> {
const combined = combineSeries(histories, metric);
value = combined.value;
history = combined.history;
gpuName = `GPU ${ids.join("+")}`;
gpuName = ids.length > 0 ? `GPU ${ids.join("+")}` : "NO GPUS";
} else {
const gpuId = state.settings.gpuId ?? "all";
value = poller?.getValue(gpuId, metric);
@@ -108,9 +108,6 @@ export class GpuGraph extends SingletonAction<GpuSettings> {
}
function parseCombo(raw: string | undefined): string[] | undefined {
const ids = (raw ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const ids = [...new Set((raw ?? "").split(",").map((s) => s.trim()).filter(Boolean))];
return ids.length > 0 ? ids : undefined;
}
+21 -24
View File
@@ -24,6 +24,7 @@ type InflightState = {
unwatchStats?: () => void;
history: number[];
sampler?: ReturnType<typeof setInterval>;
lastSvg?: string;
};
const HISTORY_LIMIT = 60;
@@ -106,38 +107,34 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
private render(state: InflightState): void {
const action = state.action;
if (!action) return;
let svg: string;
if (this.displayOf(state) === "usage") {
const modelKey = this.modelKeyOf(state);
const offline = runtime.offline;
void action.setImage(
svgDataUrl(
renderUsage({
modelName: modelKey,
stats: runtime.getStats(modelKey),
primaryStat: this.primaryStatOf(state),
offline,
}),
),
svg = svgDataUrl(
renderUsage({
modelName: modelKey,
stats: runtime.getStats(modelKey),
primaryStat: this.primaryStatOf(state),
offline: runtime.offline,
}),
);
return;
}
const modelId = state.settings.modelId ?? "";
const trackerState = modelId === "all" ? "ready" : runtime.tracker.state(modelId);
const count = modelId === "all" ? runtime.tracker.total() : runtime.tracker.count(modelId);
const offline = runtime.offline && modelId.length > 0;
const modelName = modelId === "" ? "unset" : modelId;
void action.setImage(
svgDataUrl(
} else {
const modelId = state.settings.modelId ?? "";
const trackerState = modelId === "all" ? "ready" : runtime.tracker.state(modelId);
const count = modelId === "all" ? runtime.tracker.total() : runtime.tracker.count(modelId);
svg = svgDataUrl(
renderInflight({
modelName,
modelName: modelId === "" ? "unset" : modelId,
state: trackerState,
count,
offline,
offline: runtime.offline && modelId.length > 0,
history: state.history,
}),
),
);
);
}
if (svg === state.lastSvg) return;
state.lastSvg = svg;
void action.setImage(svg);
}
private sample(state: InflightState): void {
+2 -5
View File
@@ -12,7 +12,6 @@ class Runtime {
private feed?: EventFeed;
private poller?: MetricsPoller;
private statsCache?: StatsCache;
private statsUnsub?: () => void;
private listeners = new Set<() => void>();
ensureConnections(cfg: LlamaSwapConfig): void {
@@ -22,9 +21,7 @@ class Runtime {
this.poller?.stop();
this.feed = undefined;
this.poller = undefined;
this.statsUnsub?.();
this.statsCache = undefined;
this.statsUnsub = undefined;
this.statsCache?.setConfig(cfg);
this.cfg = cfg;
}
if (!this.feed) {
@@ -46,7 +43,7 @@ class Runtime {
}
if (!this.statsCache) {
this.statsCache = new StatsCache(cfg);
this.statsUnsub = this.statsCache.onChange(() => this.emit());
this.statsCache.onChange(() => this.emit());
}
}
+27 -14
View File
@@ -7,7 +7,8 @@ const ACTIVITY_THROTTLE_MS = 2000;
export type FetchFn = (cfg: LlamaSwapConfig, modelId: string) => Promise<UsageStats | null>;
export class StatsCache {
private keys = new Set<string>();
private refs = new Map<string, number>();
private refreshing = false;
private values = new Map<string, UsageStats | undefined>();
private listeners = new Set<() => void>();
private timer?: ReturnType<typeof setInterval>;
@@ -27,8 +28,7 @@ export class StatsCache {
}
register(key: string): void {
if (this.keys.has(key)) return;
this.keys.add(key);
this.refs.set(key, (this.refs.get(key) ?? 0) + 1);
if (!this.timer) {
void this.refresh();
this.timer = setInterval(() => void this.refresh(), this.pollMs);
@@ -36,11 +36,18 @@ export class StatsCache {
}
unregister(key: string): void {
this.keys.delete(key);
this.values.delete(key);
if (this.keys.size === 0 && this.timer) {
clearInterval(this.timer);
const count = (this.refs.get(key) ?? 0) - 1;
if (count <= 0) {
this.refs.delete(key);
this.values.delete(key);
} else {
this.refs.set(key, count);
}
if (this.refs.size === 0) {
if (this.timer) clearInterval(this.timer);
this.timer = undefined;
if (this.throttleTimer) clearTimeout(this.throttleTimer);
this.throttleTimer = undefined;
}
}
@@ -59,15 +66,21 @@ export class StatsCache {
}
async refresh(): Promise<void> {
this.lastRefresh = Date.now();
for (const key of this.keys) {
try {
const stats = await this.fetchFn(this.cfg, key);
if (stats) this.values.set(key, stats);
} catch {
if (this.refreshing) return;
this.refreshing = true;
try {
for (const key of this.refs.keys()) {
try {
const stats = await this.fetchFn(this.cfg, key);
if (stats) this.values.set(key, stats);
} catch {
}
}
} finally {
this.refreshing = false;
this.lastRefresh = Date.now();
this.emit();
}
this.emit();
}
onChange(listener: () => void): () => void {
+23
View File
@@ -83,3 +83,26 @@ test("fetch failure keeps the last-known value", async () => {
assert.equal(cache.get("a"), undefined);
cache.unregister("a");
});
test("ref-counted register: a sibling unregister keeps the key active", async () => {
const calls = { count: 0 };
const cache = new StatsCache(cfg, stubFetch({ totalRequests: 5, totalInputTokens: 1, totalOutputTokens: 1, genP95: 1 }, calls), 10000, 30);
cache.register("all");
cache.register("all");
cache.unregister("all");
await flush();
assert.equal(cache.get("all")!.totalRequests, 5);
assert.equal(calls.count, 1);
cache.unregister("all");
assert.equal(cache.get("all"), undefined);
});
test("setConfig keeps registrations but clears cached values", async () => {
const cache = new StatsCache(cfg, async (_c, key) => ({ totalRequests: 5, totalInputTokens: 1, totalOutputTokens: 1, genP95: 1 }), 10000, 30);
cache.register("all");
await flush();
assert.equal(cache.get("all")!.totalRequests, 5);
cache.setConfig({ baseUrl: "http://new" });
assert.equal(cache.get("all"), undefined);
cache.unregister("all");
});