fix: refcount StatsCache keys, keep registrations across config change, and tidy GPU/usage render paths
This commit is contained in:
@@ -87,7 +87,7 @@ export class GpuGraph extends SingletonAction<GpuSettings> {
|
|||||||
const combined = combineSeries(histories, metric);
|
const combined = combineSeries(histories, metric);
|
||||||
value = combined.value;
|
value = combined.value;
|
||||||
history = combined.history;
|
history = combined.history;
|
||||||
gpuName = `GPU ${ids.join("+")}`;
|
gpuName = ids.length > 0 ? `GPU ${ids.join("+")}` : "NO GPUS";
|
||||||
} else {
|
} else {
|
||||||
const gpuId = state.settings.gpuId ?? "all";
|
const gpuId = state.settings.gpuId ?? "all";
|
||||||
value = poller?.getValue(gpuId, metric);
|
value = poller?.getValue(gpuId, metric);
|
||||||
@@ -108,9 +108,6 @@ export class GpuGraph extends SingletonAction<GpuSettings> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseCombo(raw: string | undefined): string[] | undefined {
|
function parseCombo(raw: string | undefined): string[] | undefined {
|
||||||
const ids = (raw ?? "")
|
const ids = [...new Set((raw ?? "").split(",").map((s) => s.trim()).filter(Boolean))];
|
||||||
.split(",")
|
|
||||||
.map((s) => s.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
return ids.length > 0 ? ids : undefined;
|
return ids.length > 0 ? ids : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ type InflightState = {
|
|||||||
unwatchStats?: () => void;
|
unwatchStats?: () => void;
|
||||||
history: number[];
|
history: number[];
|
||||||
sampler?: ReturnType<typeof setInterval>;
|
sampler?: ReturnType<typeof setInterval>;
|
||||||
|
lastSvg?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const HISTORY_LIMIT = 60;
|
const HISTORY_LIMIT = 60;
|
||||||
@@ -106,39 +107,35 @@ export class InflightMonitor extends SingletonAction<InflightSettings> {
|
|||||||
private render(state: InflightState): void {
|
private render(state: InflightState): void {
|
||||||
const action = state.action;
|
const action = state.action;
|
||||||
if (!action) return;
|
if (!action) return;
|
||||||
|
let svg: string;
|
||||||
if (this.displayOf(state) === "usage") {
|
if (this.displayOf(state) === "usage") {
|
||||||
const modelKey = this.modelKeyOf(state);
|
const modelKey = this.modelKeyOf(state);
|
||||||
const offline = runtime.offline;
|
svg = svgDataUrl(
|
||||||
void action.setImage(
|
|
||||||
svgDataUrl(
|
|
||||||
renderUsage({
|
renderUsage({
|
||||||
modelName: modelKey,
|
modelName: modelKey,
|
||||||
stats: runtime.getStats(modelKey),
|
stats: runtime.getStats(modelKey),
|
||||||
primaryStat: this.primaryStatOf(state),
|
primaryStat: this.primaryStatOf(state),
|
||||||
offline,
|
offline: runtime.offline,
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
return;
|
} else {
|
||||||
}
|
|
||||||
|
|
||||||
const modelId = state.settings.modelId ?? "";
|
const modelId = state.settings.modelId ?? "";
|
||||||
const trackerState = modelId === "all" ? "ready" : runtime.tracker.state(modelId);
|
const trackerState = modelId === "all" ? "ready" : runtime.tracker.state(modelId);
|
||||||
const count = modelId === "all" ? runtime.tracker.total() : runtime.tracker.count(modelId);
|
const count = modelId === "all" ? runtime.tracker.total() : runtime.tracker.count(modelId);
|
||||||
const offline = runtime.offline && modelId.length > 0;
|
svg = svgDataUrl(
|
||||||
const modelName = modelId === "" ? "unset" : modelId;
|
|
||||||
void action.setImage(
|
|
||||||
svgDataUrl(
|
|
||||||
renderInflight({
|
renderInflight({
|
||||||
modelName,
|
modelName: modelId === "" ? "unset" : modelId,
|
||||||
state: trackerState,
|
state: trackerState,
|
||||||
count,
|
count,
|
||||||
offline,
|
offline: runtime.offline && modelId.length > 0,
|
||||||
history: state.history,
|
history: state.history,
|
||||||
}),
|
}),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (svg === state.lastSvg) return;
|
||||||
|
state.lastSvg = svg;
|
||||||
|
void action.setImage(svg);
|
||||||
|
}
|
||||||
|
|
||||||
private sample(state: InflightState): void {
|
private sample(state: InflightState): void {
|
||||||
const modelId = state.settings.modelId ?? "";
|
const modelId = state.settings.modelId ?? "";
|
||||||
|
|||||||
+2
-5
@@ -12,7 +12,6 @@ class Runtime {
|
|||||||
private feed?: EventFeed;
|
private feed?: EventFeed;
|
||||||
private poller?: MetricsPoller;
|
private poller?: MetricsPoller;
|
||||||
private statsCache?: StatsCache;
|
private statsCache?: StatsCache;
|
||||||
private statsUnsub?: () => void;
|
|
||||||
private listeners = new Set<() => void>();
|
private listeners = new Set<() => void>();
|
||||||
|
|
||||||
ensureConnections(cfg: LlamaSwapConfig): void {
|
ensureConnections(cfg: LlamaSwapConfig): void {
|
||||||
@@ -22,9 +21,7 @@ class Runtime {
|
|||||||
this.poller?.stop();
|
this.poller?.stop();
|
||||||
this.feed = undefined;
|
this.feed = undefined;
|
||||||
this.poller = undefined;
|
this.poller = undefined;
|
||||||
this.statsUnsub?.();
|
this.statsCache?.setConfig(cfg);
|
||||||
this.statsCache = undefined;
|
|
||||||
this.statsUnsub = undefined;
|
|
||||||
this.cfg = cfg;
|
this.cfg = cfg;
|
||||||
}
|
}
|
||||||
if (!this.feed) {
|
if (!this.feed) {
|
||||||
@@ -46,7 +43,7 @@ class Runtime {
|
|||||||
}
|
}
|
||||||
if (!this.statsCache) {
|
if (!this.statsCache) {
|
||||||
this.statsCache = new StatsCache(cfg);
|
this.statsCache = new StatsCache(cfg);
|
||||||
this.statsUnsub = this.statsCache.onChange(() => this.emit());
|
this.statsCache.onChange(() => this.emit());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-8
@@ -7,7 +7,8 @@ const ACTIVITY_THROTTLE_MS = 2000;
|
|||||||
export type FetchFn = (cfg: LlamaSwapConfig, modelId: string) => Promise<UsageStats | null>;
|
export type FetchFn = (cfg: LlamaSwapConfig, modelId: string) => Promise<UsageStats | null>;
|
||||||
|
|
||||||
export class StatsCache {
|
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 values = new Map<string, UsageStats | undefined>();
|
||||||
private listeners = new Set<() => void>();
|
private listeners = new Set<() => void>();
|
||||||
private timer?: ReturnType<typeof setInterval>;
|
private timer?: ReturnType<typeof setInterval>;
|
||||||
@@ -27,8 +28,7 @@ export class StatsCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
register(key: string): void {
|
register(key: string): void {
|
||||||
if (this.keys.has(key)) return;
|
this.refs.set(key, (this.refs.get(key) ?? 0) + 1);
|
||||||
this.keys.add(key);
|
|
||||||
if (!this.timer) {
|
if (!this.timer) {
|
||||||
void this.refresh();
|
void this.refresh();
|
||||||
this.timer = setInterval(() => void this.refresh(), this.pollMs);
|
this.timer = setInterval(() => void this.refresh(), this.pollMs);
|
||||||
@@ -36,11 +36,18 @@ export class StatsCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unregister(key: string): void {
|
unregister(key: string): void {
|
||||||
this.keys.delete(key);
|
const count = (this.refs.get(key) ?? 0) - 1;
|
||||||
|
if (count <= 0) {
|
||||||
|
this.refs.delete(key);
|
||||||
this.values.delete(key);
|
this.values.delete(key);
|
||||||
if (this.keys.size === 0 && this.timer) {
|
} else {
|
||||||
clearInterval(this.timer);
|
this.refs.set(key, count);
|
||||||
|
}
|
||||||
|
if (this.refs.size === 0) {
|
||||||
|
if (this.timer) clearInterval(this.timer);
|
||||||
this.timer = undefined;
|
this.timer = undefined;
|
||||||
|
if (this.throttleTimer) clearTimeout(this.throttleTimer);
|
||||||
|
this.throttleTimer = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,16 +66,22 @@ export class StatsCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async refresh(): Promise<void> {
|
async refresh(): Promise<void> {
|
||||||
this.lastRefresh = Date.now();
|
if (this.refreshing) return;
|
||||||
for (const key of this.keys) {
|
this.refreshing = true;
|
||||||
|
try {
|
||||||
|
for (const key of this.refs.keys()) {
|
||||||
try {
|
try {
|
||||||
const stats = await this.fetchFn(this.cfg, key);
|
const stats = await this.fetchFn(this.cfg, key);
|
||||||
if (stats) this.values.set(key, stats);
|
if (stats) this.values.set(key, stats);
|
||||||
} catch {
|
} catch {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
this.refreshing = false;
|
||||||
|
this.lastRefresh = Date.now();
|
||||||
this.emit();
|
this.emit();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onChange(listener: () => void): () => void {
|
onChange(listener: () => void): () => void {
|
||||||
this.listeners.add(listener);
|
this.listeners.add(listener);
|
||||||
|
|||||||
@@ -83,3 +83,26 @@ test("fetch failure keeps the last-known value", async () => {
|
|||||||
assert.equal(cache.get("a"), undefined);
|
assert.equal(cache.get("a"), undefined);
|
||||||
cache.unregister("a");
|
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");
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user