Restore bottom performance history graphs
This commit is contained in:
Vendored
+90
-4
@@ -9,6 +9,7 @@
|
||||
var API = "/api/plugins/ollama-manager";
|
||||
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";
|
||||
|
||||
function readSavedPlacements() {
|
||||
try {
|
||||
@@ -20,6 +21,16 @@
|
||||
function savePlacements(value) {
|
||||
try { window.localStorage.setItem(PLACEMENT_STORAGE_KEY, JSON.stringify(value || {})); } catch (_) {}
|
||||
}
|
||||
function readSavedPerformanceSamples() {
|
||||
try {
|
||||
var raw = window.localStorage.getItem(PERFORMANCE_STORAGE_KEY);
|
||||
var value = raw ? JSON.parse(raw) : [];
|
||||
return Array.isArray(value) ? value.filter(function (sample) { return sample && typeof sample.captured_at === "number"; }).slice(-120) : [];
|
||||
} catch (_) { return []; }
|
||||
}
|
||||
function savePerformanceSamples(value) {
|
||||
try { window.localStorage.setItem(PERFORMANCE_STORAGE_KEY, JSON.stringify((value || []).slice(-120))); } catch (_) {}
|
||||
}
|
||||
function readSavedChat() {
|
||||
try {
|
||||
var raw = window.localStorage.getItem(CHAT_STORAGE_KEY);
|
||||
@@ -217,6 +228,58 @@
|
||||
);
|
||||
}
|
||||
|
||||
function graphNumber(sample, key) {
|
||||
if (!sample || sample[key] === null || sample[key] === undefined) return null;
|
||||
var value = Number(sample[key]);
|
||||
return isFinite(value) ? value : null;
|
||||
}
|
||||
function PerformanceGraph(props) {
|
||||
var samples = props.samples || [], values = samples.map(function (sample) { return graphNumber(sample, props.valueKey); });
|
||||
var max = Number(props.max || 0), observed = values.reduce(function (result, value) { return value == null ? result : Math.max(result, value); }, 0);
|
||||
if (!max) max = observed > 0 ? observed * 1.15 : 1;
|
||||
var width = 240, height = 84, pad = 5, points = [], latest = null;
|
||||
values.forEach(function (value, index) {
|
||||
if (value == null) return;
|
||||
latest = value;
|
||||
var x = pad + (values.length > 1 ? index * (width - pad * 2) / (values.length - 1) : (width / 2));
|
||||
var y = height - pad - Math.max(0, Math.min(1, value / max)) * (height - pad * 2);
|
||||
points.push(x.toFixed(1) + "," + y.toFixed(1));
|
||||
});
|
||||
var line = points.join(" "), area = points.length ? pad + "," + (height - pad) + " " + line + " " + (width - pad) + "," + (height - pad) : "";
|
||||
var formatted = latest == null ? "No samples yet" : (props.format ? props.format(latest) : Number(latest).toFixed(1));
|
||||
return h("article", { className: "ollama-performance-graph" },
|
||||
h("div", { className: "ollama-performance-graph-heading" }, h("div", null, h("strong", null, props.title), h("small", null, props.subtitle)), h("b", null, formatted)),
|
||||
h("svg", { className: "ollama-performance-svg", viewBox: "0 0 240 84", role: "img", "aria-label": props.title + " history" },
|
||||
h("line", { x1: pad, y1: height - pad, x2: width - pad, y2: height - pad, className: "ollama-graph-grid" }),
|
||||
h("line", { x1: pad, y1: height / 2, x2: width - pad, y2: height / 2, className: "ollama-graph-grid" }),
|
||||
h("line", { x1: pad, y1: pad, x2: width - pad, y2: pad, className: "ollama-graph-grid" }),
|
||||
area && h("polygon", { points: area, className: "ollama-graph-area" }),
|
||||
line && h("polyline", { points: line, className: "ollama-graph-line" })
|
||||
),
|
||||
h("div", { className: "ollama-performance-graph-scale" }, h("span", null, "0"), h("span", null, props.range || "dynamic"))
|
||||
);
|
||||
}
|
||||
function PerformanceGraphs(props) {
|
||||
var samples = props.samples || [];
|
||||
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-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 })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function RuntimePanel(props) {
|
||||
var runtime = props.runtime || {}, total = Number(runtime.memory_total_bytes || 0), used = Number(runtime.memory_used_bytes || 0), pct = total ? Math.min(100, used * 100 / total) : 0;
|
||||
var gpu = runtime.gpu || {}, cpu = runtime.cpu || {}, disk = runtime.disk || {}, models = runtime.model_memory || [], loading = runtime.model_loading || [], cores = cpu.cores || [], gpus = gpu.gpus || [];
|
||||
@@ -323,7 +386,7 @@
|
||||
var metricsState = React.useState(null), metrics = metricsState[0], setMetrics = metricsState[1];
|
||||
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([]), samples = samplesState[0], setSamples = samplesState[1];
|
||||
var samplesState = React.useState(readSavedPerformanceSamples()), samples = samplesState[0], setSamples = samplesState[1];
|
||||
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];
|
||||
@@ -411,8 +474,26 @@
|
||||
refreshConversations();
|
||||
setNotice({ ok: "Conversation deleted from shared storage." });
|
||||
}
|
||||
function pollRuntime() { fetchJSON(API + "/runtime").then(function (value) { setRuntime(value); setSamples(function (old) { return old.concat([{ used: Number(value.memory_used_bytes || 0), total: Number(value.memory_total_bytes || 0) }]).slice(-60); }); }).catch(function () {}); }
|
||||
React.useEffect(function () { pollRuntime(); var timer = setInterval(pollRuntime, 5000); return function () { clearInterval(timer); }; }, []);
|
||||
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); });
|
||||
}).catch(function () {});
|
||||
}
|
||||
React.useEffect(function () { pollRuntime(); var timer = setInterval(pollRuntime, 1000); return function () { clearInterval(timer); }; }, []);
|
||||
React.useEffect(function () { savePerformanceSamples(samples); }, [samples]);
|
||||
|
||||
React.useEffect(function () { savePlacements(placements); }, [placements]);
|
||||
function toggleIn(setter, name) { setter(function (old) { return old.indexOf(name) >= 0 ? old.filter(function (item) { return item !== name; }) : old.concat([name]); }); }
|
||||
@@ -549,8 +630,13 @@
|
||||
h("div", { className: "ollama-controls-heading" }, h("h3", null, "Chat controls"), h("small", null, "Model, quality, and runtime")),
|
||||
h(ModelPoolPanel, { models: models, loadedModels: loadedModels, selectedModels: selectedModels, primaryModel: selectedModels[0] || "", validatorModels: selectedModels.slice(1), poolSelection: poolSelection, placements: placements, busy: busy, onTogglePool: togglePoolModel, onPlacementChange: setPlacement, onToggleChat: toggleChatModel, onPrimaryChange: setPrimaryModel, onLoad: loadModel, onUnload: unloadModels }),
|
||||
h("details", { className: "ollama-advanced-control" }, h("summary", null, "Chat storage"), h(StoragePanel, { storage: storage, onConfigure: configureStorage, onInstall: installPostgres, onRefresh: refreshStorage })),
|
||||
h("details", { className: "ollama-advanced-control" }, h("summary", null, "Runtime telemetry"), h(RuntimePanel, { runtime: runtime, samples: samples }))
|
||||
h("div", { className: "ollama-advanced-note" }, "Runtime cards and historical performance graphs are shown in Operations / Performance below.")
|
||||
)
|
||||
),
|
||||
h("section", { className: "ollama-operations-section" },
|
||||
h("div", { className: "ollama-operations-heading" }, h("div", null, h("div", { className: "ollama-eyebrow" }, "OPERATIONS / PERFORMANCE"), h("h3", null, "Runtime telemetry and historical graphs"), h("p", null, "The complete operational view is preserved below the chat so it remains available without competing with the conversation.")), h("span", null, "CPU · GPU · RAM · disk · swap")),
|
||||
h(RuntimePanel, { runtime: runtime, samples: samples }),
|
||||
h(PerformanceGraphs, { samples: samples })
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user