feat: add selectable performance history windows

This commit is contained in:
Hermes Agent
2026-08-29 19:34:41 +10:00
parent f914dd0eca
commit a781e3d4de
7 changed files with 177 additions and 32 deletions
+39 -27
View File
@@ -10,6 +10,13 @@
var CHAT_STORAGE_KEY = "hermes.ollama-manager.chat.v1";
var PLACEMENT_STORAGE_KEY = "hermes.ollama-manager.placement.v1";
var PERFORMANCE_STORAGE_KEY = "hermes.ollama-manager.performance.v1";
var PERFORMANCE_WINDOWS = [
{ value: "1", label: "Last 1 hour", seconds: 60 * 60 },
{ value: "6", label: "Last 6 hours", seconds: 6 * 60 * 60 },
{ value: "9", label: "Last 9 hours", seconds: 9 * 60 * 60 },
{ value: "12", label: "Last 12 hours", seconds: 12 * 60 * 60 },
{ value: "24", label: "Last 24 hours", seconds: 24 * 60 * 60 }
];
function readSavedPlacements() {
try {
@@ -261,21 +268,31 @@
}
function PerformanceGraphs(props) {
var samples = props.samples || [];
var rangeState = React.useState("1"), range = rangeState[0], setRange = rangeState[1];
var selectedWindow = PERFORMANCE_WINDOWS.find(function (item) { return item.value === range; }) || PERFORMANCE_WINDOWS[0];
var cutoff = Date.now() / 1000 - selectedWindow.seconds;
var visibleSamples = samples.filter(function (sample) { return Number(sample.captured_at || 0) >= cutoff; });
function pct(value) { return value == null ? "n/a" : Number(value).toFixed(1) + "%"; }
function gib(value) { return value == null ? "n/a" : Number(value).toFixed(2) + " GiB"; }
function count(value) { return value == null ? "n/a" : String(Math.round(value)); }
return h("section", { className: "ollama-performance-graphs" },
h("div", { className: "ollama-performance-graphs-heading" }, h("div", null, h("h3", null, "Performance history"), h("p", null, "CPU, GPU, memory, storage, swap, and Ollama residency over the last ", samples.length, " samples.")), h("span", null, samples.length ? "Live · 1 second" : "Waiting for telemetry")),
h("div", { className: "ollama-performance-graphs-heading" },
h("div", null, h("h3", null, "Performance history"), h("p", null, "CPU, GPU, memory, storage, swap, and Ollama residency over the selected window. ", visibleSamples.length, " minute samples available.")),
h("div", { className: "ollama-performance-controls" },
h("label", { className: "ollama-performance-range" }, "History window", h("select", { value: range, onChange: function (event) { setRange(event.target.value); }, "aria-label": "Performance history window" }, PERFORMANCE_WINDOWS.map(function (item) { return h("option", { key: item.value, value: item.value }, item.label); }))),
h("span", null, samples.length ? "Live · 1 second" : "Waiting for telemetry")
)
),
h("div", { className: "ollama-performance-grid" },
h(PerformanceGraph, { title: "CPU usage", subtitle: "Total processor utilization", valueKey: "cpu_usage_percent", max: 100, range: "100%", format: pct, samples: samples }),
h(PerformanceGraph, { title: "CPU load", subtitle: "1-minute load average", valueKey: "cpu_load_1m", range: "dynamic", format: function (value) { return Number(value).toFixed(2); }, samples: samples }),
h(PerformanceGraph, { title: "System memory", subtitle: "Used RAM", valueKey: "memory_used_percent", max: 100, range: "100%", format: pct, samples: samples }),
h(PerformanceGraph, { title: "GPU usage", subtitle: "Aggregate GPU utilization", valueKey: "gpu_usage_percent", max: 100, range: "100%", format: pct, samples: samples }),
h(PerformanceGraph, { title: "GPU VRAM", subtitle: "Used video memory", valueKey: "gpu_vram_used_percent", max: 100, range: "100%", format: pct, samples: samples }),
h(PerformanceGraph, { title: "Disk usage", subtitle: "Root filesystem", valueKey: "disk_used_percent", max: 100, range: "100%", format: pct, samples: samples }),
h(PerformanceGraph, { title: "Swap usage", subtitle: "Used swap memory", valueKey: "swap_used_percent", max: 100, range: "100%", format: pct, samples: samples }),
h(PerformanceGraph, { title: "Ollama model weights", subtitle: "Resident model bytes", valueKey: "ollama_model_gib", range: "dynamic", format: gib, samples: samples }),
h(PerformanceGraph, { title: "Resident models", subtitle: "Loaded Ollama model count", valueKey: "resident_model_count", range: "dynamic", format: count, samples: samples })
h(PerformanceGraph, { title: "CPU usage", subtitle: "Total processor utilization", valueKey: "cpu_usage_percent", max: 100, range: "100%", format: pct, samples: visibleSamples }),
h(PerformanceGraph, { title: "CPU load", subtitle: "1-minute load average", valueKey: "cpu_load_1m", range: "dynamic", format: function (value) { return Number(value).toFixed(2); }, samples: visibleSamples }),
h(PerformanceGraph, { title: "System memory", subtitle: "Used RAM", valueKey: "memory_used_percent", max: 100, range: "100%", format: pct, samples: visibleSamples }),
h(PerformanceGraph, { title: "GPU usage", subtitle: "Aggregate GPU utilization", valueKey: "gpu_usage_percent", max: 100, range: "100%", format: pct, samples: visibleSamples }),
h(PerformanceGraph, { title: "GPU VRAM", subtitle: "Used video memory", valueKey: "gpu_vram_used_percent", max: 100, range: "100%", format: pct, samples: visibleSamples }),
h(PerformanceGraph, { title: "Disk usage", subtitle: "Root filesystem", valueKey: "disk_used_percent", max: 100, range: "100%", format: pct, samples: visibleSamples }),
h(PerformanceGraph, { title: "Swap usage", subtitle: "Used swap memory", valueKey: "swap_used_percent", max: 100, range: "100%", format: pct, samples: visibleSamples }),
h(PerformanceGraph, { title: "Ollama model weights", subtitle: "Resident model bytes", valueKey: "ollama_model_gib", range: "dynamic", format: gib, samples: visibleSamples }),
h(PerformanceGraph, { title: "Resident models", subtitle: "Loaded Ollama model count", valueKey: "resident_model_count", range: "dynamic", format: count, samples: visibleSamples })
)
);
}
@@ -387,6 +404,7 @@
var aggregateState = React.useState(null), aggregate = aggregateState[0], setAggregate = aggregateState[1];
var runtimeState = React.useState(null), runtime = runtimeState[0], setRuntime = runtimeState[1];
var samplesState = React.useState(readSavedPerformanceSamples()), samples = samplesState[0], setSamples = samplesState[1];
var performanceHistoryAt = React.useRef(0);
var busyState = React.useState(""), busy = busyState[0], setBusy = busyState[1];
var noticeState = React.useState(null), notice = noticeState[0], setNotice = noticeState[1];
var validationState = React.useState(null), validationReports = validationState[0], setValidationReports = validationState[1];
@@ -474,25 +492,19 @@
refreshConversations();
setNotice({ ok: "Conversation deleted from shared storage." });
}
function pollRuntime() {
fetchJSON(API + "/runtime").then(function (value) {
var total = Number(value.memory_total_bytes || 0), used = Number(value.memory_used_bytes || 0), gpu = value.gpu || {}, gpus = gpu.gpus || [], gpuTotal = gpus.reduce(function (sum, item) { return sum + Number(item.total_bytes || 0); }, 0), gpuUsed = gpus.reduce(function (sum, item) { return sum + Number(item.used_bytes || 0); }, 0), swapTotal = Number(value.swap_total_bytes || 0), swapUsed = Number(value.swap_used_bytes || 0);
setRuntime(value);
setSamples(function (old) { return old.concat([{
captured_at: Number(value.captured_at || Date.now() / 1000),
cpu_usage_percent: value.cpu && value.cpu.usage_percent,
cpu_load_1m: value.cpu && value.cpu.load_average && value.cpu.load_average[0],
memory_used_percent: total ? used * 100 / total : null,
gpu_usage_percent: gpu.utilization_percent,
gpu_vram_used_percent: gpuTotal ? gpuUsed * 100 / gpuTotal : null,
disk_used_percent: value.disk && value.disk.used_percent,
swap_used_percent: swapTotal ? swapUsed * 100 / swapTotal : null,
ollama_model_gib: Number(value.ollama_model_bytes || 0) / (1024 * 1024 * 1024),
resident_model_count: Array.isArray(value.model_memory) ? value.model_memory.length : 0
}]).slice(-120); });
function pollPerformanceHistory() {
performanceHistoryAt.current = Date.now();
fetchJSON(API + "/runtime/history?hours=24").then(function (value) {
setSamples(Array.isArray(value.samples) ? value.samples : []);
}).catch(function () {});
}
React.useEffect(function () { pollRuntime(); var timer = setInterval(pollRuntime, 1000); return function () { clearInterval(timer); }; }, []);
function pollRuntime() {
fetchJSON(API + "/runtime").then(function (value) {
setRuntime(value);
if (Date.now() - performanceHistoryAt.current >= 5000) pollPerformanceHistory();
}).catch(function () {});
}
React.useEffect(function () { pollRuntime(); pollPerformanceHistory(); var timer = setInterval(pollRuntime, 1000); return function () { clearInterval(timer); }; }, []);
React.useEffect(function () { savePerformanceSamples(samples); }, [samples]);
React.useEffect(function () { savePlacements(placements); }, [placements]);
+1 -1
View File
File diff suppressed because one or more lines are too long