smooth out metrics
This commit is contained in:
@@ -37,6 +37,7 @@ let metricsPageActive = true;
|
|||||||
const GEOJSON_URL = "/vendor/geo-countries/countries.geojson";
|
const GEOJSON_URL = "/vendor/geo-countries/countries.geojson";
|
||||||
const MAX_CHART_POINTS = 240;
|
const MAX_CHART_POINTS = 240;
|
||||||
const METRICS_POLL_INTERVAL_MS = 5000;
|
const METRICS_POLL_INTERVAL_MS = 5000;
|
||||||
|
const CHART_ANIMATION_MS = window.matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 480;
|
||||||
const SESSION_LABELS = ["Console", "Remote Desktop", "Files", "Processes"];
|
const SESSION_LABELS = ["Console", "Remote Desktop", "Files", "Processes"];
|
||||||
const SESSION_COLORS = ["#34d399", "#c084fc", "#60a5fa", "#fb923c"];
|
const SESSION_COLORS = ["#34d399", "#c084fc", "#60a5fa", "#fb923c"];
|
||||||
const SESSION_EMPTY_COLOR = "rgba(100, 116, 139, 0.35)";
|
const SESSION_EMPTY_COLOR = "rgba(100, 116, 139, 0.35)";
|
||||||
@@ -81,34 +82,55 @@ function chartTooltip() {
|
|||||||
bodyColor: "#cbd5e1",
|
bodyColor: "#cbd5e1",
|
||||||
borderColor: "#334155",
|
borderColor: "#334155",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
|
padding: 10,
|
||||||
|
displayColors: true,
|
||||||
|
callbacks: {
|
||||||
|
title: (items) => {
|
||||||
|
const timestamp = Number(items?.[0]?.parsed?.x);
|
||||||
|
return Number.isFinite(timestamp) ? formatChartTime(timestamp, true) : "";
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function lineChartOptions(extra = {}) {
|
function lineChartOptions(extra = {}) {
|
||||||
const extraPlugins = extra.plugins || {};
|
const {
|
||||||
|
plugins: extraPlugins = {},
|
||||||
|
scales: extraScales = {},
|
||||||
|
...extraOptions
|
||||||
|
} = extra;
|
||||||
return {
|
return {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
animation: false,
|
normalized: true,
|
||||||
|
parsing: false,
|
||||||
|
animation: {
|
||||||
|
duration: CHART_ANIMATION_MS,
|
||||||
|
easing: "easeOutQuart",
|
||||||
|
},
|
||||||
interaction: { mode: "index", intersect: false },
|
interaction: { mode: "index", intersect: false },
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
|
grace: "8%",
|
||||||
ticks: { color: "#94a3b8" },
|
ticks: { color: "#94a3b8" },
|
||||||
grid: { color: "rgba(100, 116, 139, 0.1)" },
|
grid: { color: "rgba(100, 116, 139, 0.1)" },
|
||||||
},
|
},
|
||||||
x: {
|
x: {
|
||||||
|
type: "linear",
|
||||||
|
bounds: "data",
|
||||||
ticks: {
|
ticks: {
|
||||||
color: "#94a3b8",
|
color: "#94a3b8",
|
||||||
maxTicksLimit: 6,
|
count: 6,
|
||||||
autoSkip: true,
|
|
||||||
maxRotation: 0,
|
maxRotation: 0,
|
||||||
|
callback: (value) => formatChartTime(Number(value), true),
|
||||||
},
|
},
|
||||||
grid: { color: "rgba(100, 116, 139, 0.1)" },
|
grid: { color: "rgba(100, 116, 139, 0.1)" },
|
||||||
},
|
},
|
||||||
...(extra.scales || {}),
|
...extraScales,
|
||||||
},
|
},
|
||||||
...extra,
|
...extraOptions,
|
||||||
plugins: {
|
plugins: {
|
||||||
legend:
|
legend:
|
||||||
extraPlugins.legend ?? { display: true, labels: { boxWidth: 10, boxHeight: 10 } },
|
extraPlugins.legend ?? { display: true, labels: { boxWidth: 10, boxHeight: 10 } },
|
||||||
@@ -166,107 +188,59 @@ function initCharts() {
|
|||||||
const sessionsCtx = document.getElementById("sessions-chart");
|
const sessionsCtx = document.getElementById("sessions-chart");
|
||||||
const osCtx = document.getElementById("os-chart");
|
const osCtx = document.getElementById("os-chart");
|
||||||
|
|
||||||
clientsChart = new Chart(clientsCtx, {
|
clientsChart = makeLineChart(
|
||||||
type: "line",
|
clientsCtx,
|
||||||
data: {
|
[{
|
||||||
labels: [],
|
label: "Clients Online",
|
||||||
datasets: [
|
data: [],
|
||||||
{
|
borderColor: "rgb(96, 165, 250)",
|
||||||
label: "Clients Online",
|
backgroundColor: "rgba(96, 165, 250, 0.12)",
|
||||||
data: [],
|
fill: true,
|
||||||
borderColor: "rgb(96, 165, 250)",
|
tension: 0.35,
|
||||||
backgroundColor: "rgba(96, 165, 250, 0.1)",
|
cubicInterpolationMode: "monotone",
|
||||||
fill: true,
|
pointRadius: 0,
|
||||||
tension: 0.4,
|
pointHoverRadius: 4,
|
||||||
pointRadius: 0,
|
borderWidth: 2,
|
||||||
pointHoverRadius: 4,
|
}],
|
||||||
borderWidth: 2,
|
{
|
||||||
},
|
plugins: { legend: { display: false } },
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
animation: false,
|
|
||||||
plugins: {
|
|
||||||
legend: { display: false },
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: "#0f172a",
|
|
||||||
titleColor: "#e2e8f0",
|
|
||||||
bodyColor: "#cbd5e1",
|
|
||||||
borderColor: "#334155",
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
ticks: { color: "#94a3b8", stepSize: 1 },
|
grace: "10%",
|
||||||
grid: { color: "rgba(100, 116, 139, 0.1)" },
|
ticks: { color: "#94a3b8", precision: 0 },
|
||||||
},
|
|
||||||
x: {
|
|
||||||
ticks: {
|
|
||||||
color: "#94a3b8",
|
|
||||||
maxTicksLimit: 6,
|
|
||||||
autoSkip: true,
|
|
||||||
maxRotation: 0,
|
|
||||||
},
|
|
||||||
grid: { color: "rgba(100, 116, 139, 0.1)" },
|
grid: { color: "rgba(100, 116, 139, 0.1)" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
commandsChart = new Chart(commandsCtx, {
|
commandsChart = makeLineChart(
|
||||||
type: "line",
|
commandsCtx,
|
||||||
data: {
|
[{
|
||||||
labels: [],
|
label: "Commands/Min",
|
||||||
datasets: [
|
data: [],
|
||||||
{
|
borderColor: "rgb(192, 132, 252)",
|
||||||
label: "Commands/Min",
|
backgroundColor: "rgba(192, 132, 252, 0.12)",
|
||||||
data: [],
|
fill: true,
|
||||||
borderColor: "rgb(192, 132, 252)",
|
tension: 0.35,
|
||||||
backgroundColor: "rgba(192, 132, 252, 0.1)",
|
cubicInterpolationMode: "monotone",
|
||||||
fill: true,
|
pointRadius: 0,
|
||||||
tension: 0.4,
|
pointHoverRadius: 4,
|
||||||
pointRadius: 0,
|
borderWidth: 2,
|
||||||
pointHoverRadius: 4,
|
}],
|
||||||
borderWidth: 2,
|
{
|
||||||
},
|
plugins: { legend: { display: false } },
|
||||||
],
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false,
|
|
||||||
animation: false,
|
|
||||||
plugins: {
|
|
||||||
legend: { display: false },
|
|
||||||
tooltip: {
|
|
||||||
backgroundColor: "#0f172a",
|
|
||||||
titleColor: "#e2e8f0",
|
|
||||||
bodyColor: "#cbd5e1",
|
|
||||||
borderColor: "#334155",
|
|
||||||
borderWidth: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
scales: {
|
||||||
y: {
|
y: {
|
||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
ticks: { color: "#94a3b8" },
|
grace: "10%",
|
||||||
grid: { color: "rgba(100, 116, 139, 0.1)" },
|
ticks: { color: "#94a3b8", precision: 0 },
|
||||||
},
|
|
||||||
x: {
|
|
||||||
ticks: {
|
|
||||||
color: "#94a3b8",
|
|
||||||
maxTicksLimit: 6,
|
|
||||||
autoSkip: true,
|
|
||||||
maxRotation: 0,
|
|
||||||
},
|
|
||||||
grid: { color: "rgba(100, 116, 139, 0.1)" },
|
grid: { color: "rgba(100, 116, 139, 0.1)" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
bandwidthChart = makeLineChart(
|
bandwidthChart = makeLineChart(
|
||||||
bandwidthCtx,
|
bandwidthCtx,
|
||||||
@@ -579,11 +553,13 @@ function formatMs(value) {
|
|||||||
return `${Math.round(value)} ms`;
|
return `${Math.round(value)} ms`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTime(timestamp) {
|
function formatChartTime(timestamp, includeSeconds = false) {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
return date.toLocaleTimeString("en-US", {
|
if (!Number.isFinite(date.getTime())) return "";
|
||||||
|
return date.toLocaleTimeString([], {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
|
...(includeSeconds ? { second: "2-digit" } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1702,111 +1678,90 @@ async function initCountryMap() {
|
|||||||
await loadCountryLocations();
|
await loadCountryLocations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateLineChart(chart, points, selectors) {
|
||||||
|
if (!chart) return;
|
||||||
|
const previousLengths = chart.data.datasets.map((dataset) => dataset.data.length);
|
||||||
|
const targetData = selectors.map((selector) => points.map((point) => ({
|
||||||
|
x: point.timestamp,
|
||||||
|
y: Number(selector(point)) || 0,
|
||||||
|
})));
|
||||||
|
const needsEndpointSeed = previousLengths.some((length, index) =>
|
||||||
|
length > 0 && targetData[index].length > length);
|
||||||
|
|
||||||
|
if (needsEndpointSeed) {
|
||||||
|
for (let index = 0; index < targetData.length; index += 1) {
|
||||||
|
const previousLength = previousLengths[index];
|
||||||
|
if (previousLength === 0 || targetData[index].length <= previousLength) continue;
|
||||||
|
const seeded = targetData[index].map((point, pointIndex, series) => {
|
||||||
|
if (pointIndex < previousLength) return point;
|
||||||
|
const previous = series[Math.max(0, pointIndex - 1)];
|
||||||
|
return { x: previous.x, y: previous.y };
|
||||||
|
});
|
||||||
|
chart.data.datasets[index].data = seeded;
|
||||||
|
}
|
||||||
|
// Materialize new Chart.js elements at the prior endpoint so they never
|
||||||
|
// render for one frame at the zero baseline.
|
||||||
|
chart.update("none");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let index = 0; index < targetData.length; index += 1) {
|
||||||
|
chart.data.datasets[index].data = targetData[index];
|
||||||
|
}
|
||||||
|
chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
function updateCharts(history, snapshot) {
|
function updateCharts(history, snapshot) {
|
||||||
const points = Array.isArray(history) && history.length > 0
|
let points = Array.isArray(history)
|
||||||
? history.slice(-MAX_CHART_POINTS)
|
? history
|
||||||
|
.slice(-MAX_CHART_POINTS)
|
||||||
|
.filter((point) => Number.isFinite(Number(point?.timestamp)))
|
||||||
|
.map((point) => ({ ...point, timestamp: Number(point.timestamp) }))
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
if (points.length === 0) {
|
if (points.length === 0) {
|
||||||
const now = new Date();
|
points = [{
|
||||||
const label = formatTime(now.getTime());
|
timestamp: Date.now(),
|
||||||
|
clientsOnline: snapshot.clients.online,
|
||||||
clientsChart.data.labels = [label];
|
commandsPerMinute: snapshot.commands.lastMinute,
|
||||||
clientsChart.data.datasets[0].data = [snapshot.clients.online];
|
bandwidthSent: snapshot.bandwidth.sentPerSecond,
|
||||||
clientsChart.update("none");
|
bandwidthReceived: snapshot.bandwidth.receivedPerSecond,
|
||||||
|
httpLatencyP99: snapshot.http.latencyP99,
|
||||||
commandsChart.data.labels = [label];
|
httpLatencyP95: snapshot.http.latencyP95,
|
||||||
commandsChart.data.datasets[0].data = [snapshot.commands.lastMinute];
|
httpLatencyAvg: snapshot.http.latencyAvg,
|
||||||
commandsChart.update("none");
|
httpRequestsPerMinute: snapshot.http.lastMinute,
|
||||||
|
httpErrorsPerMinute: snapshot.http.lastMinuteErrors,
|
||||||
if (bandwidthChart) {
|
heapUsed: snapshot.server.memoryUsage.heapUsed,
|
||||||
bandwidthChart.data.labels = [label];
|
rss: snapshot.server.memoryUsage.rss,
|
||||||
bandwidthChart.data.datasets[0].data = [snapshot.bandwidth.sentPerSecond || 0];
|
systemMemoryUsedPercent: snapshot.server.systemMemory.usedPercent,
|
||||||
bandwidthChart.data.datasets[1].data = [snapshot.bandwidth.receivedPerSecond || 0];
|
eventLoopP95: snapshot.eventLoop.p95,
|
||||||
bandwidthChart.update("none");
|
eventLoopAvg: snapshot.eventLoop.avg,
|
||||||
}
|
}];
|
||||||
|
|
||||||
if (httpChart) {
|
|
||||||
httpChart.data.labels = [label];
|
|
||||||
httpChart.data.datasets[0].data = [snapshot.http.latencyP99 || 0];
|
|
||||||
httpChart.data.datasets[1].data = [snapshot.http.latencyP95 || 0];
|
|
||||||
httpChart.data.datasets[2].data = [snapshot.http.latencyAvg || 0];
|
|
||||||
httpChart.update("none");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (httpRequestsChart) {
|
|
||||||
httpRequestsChart.data.labels = [label];
|
|
||||||
httpRequestsChart.data.datasets[0].data = [snapshot.http.lastMinute || 0];
|
|
||||||
httpRequestsChart.data.datasets[1].data = [snapshot.http.lastMinuteErrors || 0];
|
|
||||||
httpRequestsChart.update("none");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (memoryChart) {
|
|
||||||
memoryChart.data.labels = [label];
|
|
||||||
memoryChart.data.datasets[0].data = [snapshot.server.memoryUsage.heapUsed || 0];
|
|
||||||
memoryChart.data.datasets[1].data = [snapshot.server.memoryUsage.rss || 0];
|
|
||||||
memoryChart.data.datasets[2].data = [snapshot.server.systemMemory.usedPercent || 0];
|
|
||||||
memoryChart.update("none");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventLoopChart) {
|
|
||||||
eventLoopChart.data.labels = [label];
|
|
||||||
eventLoopChart.data.datasets[0].data = [snapshot.eventLoop.p95 || 0];
|
|
||||||
eventLoopChart.data.datasets[1].data = [snapshot.eventLoop.avg || 0];
|
|
||||||
eventLoopChart.update("none");
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const labels = points.map((h) => formatTime(h.timestamp));
|
updateLineChart(clientsChart, points, [(point) => point.clientsOnline]);
|
||||||
const clientsData = points.map((h) => h.clientsOnline || 0);
|
updateLineChart(commandsChart, points, [(point) => point.commandsPerMinute]);
|
||||||
|
updateLineChart(bandwidthChart, points, [
|
||||||
clientsChart.data.labels = labels;
|
(point) => point.bandwidthSent,
|
||||||
clientsChart.data.datasets[0].data = clientsData;
|
(point) => point.bandwidthReceived,
|
||||||
clientsChart.update("none");
|
]);
|
||||||
|
updateLineChart(httpChart, points, [
|
||||||
const commandsData = points.map((h) => h.commandsPerMinute || 0);
|
(point) => point.httpLatencyP99,
|
||||||
|
(point) => point.httpLatencyP95,
|
||||||
commandsChart.data.labels = labels;
|
(point) => point.httpLatencyAvg,
|
||||||
commandsChart.data.datasets[0].data = commandsData;
|
]);
|
||||||
commandsChart.update("none");
|
updateLineChart(httpRequestsChart, points, [
|
||||||
|
(point) => point.httpRequestsPerMinute,
|
||||||
if (bandwidthChart) {
|
(point) => point.httpErrorsPerMinute,
|
||||||
bandwidthChart.data.labels = labels;
|
]);
|
||||||
bandwidthChart.data.datasets[0].data = points.map((h) => h.bandwidthSent || 0);
|
updateLineChart(memoryChart, points, [
|
||||||
bandwidthChart.data.datasets[1].data = points.map((h) => h.bandwidthReceived || 0);
|
(point) => point.heapUsed,
|
||||||
bandwidthChart.update("none");
|
(point) => point.rss,
|
||||||
}
|
(point) => point.systemMemoryUsedPercent,
|
||||||
|
]);
|
||||||
if (httpChart) {
|
updateLineChart(eventLoopChart, points, [
|
||||||
httpChart.data.labels = labels;
|
(point) => point.eventLoopP95,
|
||||||
httpChart.data.datasets[0].data = points.map((h) => h.httpLatencyP99 || 0);
|
(point) => point.eventLoopAvg,
|
||||||
httpChart.data.datasets[1].data = points.map((h) => h.httpLatencyP95 || 0);
|
]);
|
||||||
httpChart.data.datasets[2].data = points.map((h) => h.httpLatencyAvg || 0);
|
|
||||||
httpChart.update("none");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (httpRequestsChart) {
|
|
||||||
httpRequestsChart.data.labels = labels;
|
|
||||||
httpRequestsChart.data.datasets[0].data = points.map((h) => h.httpRequestsPerMinute || 0);
|
|
||||||
httpRequestsChart.data.datasets[1].data = points.map((h) => h.httpErrorsPerMinute || 0);
|
|
||||||
httpRequestsChart.update("none");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (memoryChart) {
|
|
||||||
memoryChart.data.labels = labels;
|
|
||||||
memoryChart.data.datasets[0].data = points.map((h) => h.heapUsed || 0);
|
|
||||||
memoryChart.data.datasets[1].data = points.map((h) => h.rss || 0);
|
|
||||||
memoryChart.data.datasets[2].data = points.map((h) => h.systemMemoryUsedPercent || 0);
|
|
||||||
memoryChart.update("none");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventLoopChart) {
|
|
||||||
eventLoopChart.data.labels = labels;
|
|
||||||
eventLoopChart.data.datasets[0].data = points.map((h) => h.eventLoopP95 || 0);
|
|
||||||
eventLoopChart.data.datasets[1].data = points.map((h) => h.eventLoopAvg || 0);
|
|
||||||
eventLoopChart.update("none");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchMetrics() {
|
async function fetchMetrics() {
|
||||||
|
|||||||
Reference in New Issue
Block a user