v0.32.6_fix2
This commit is contained in:
1 parent
e1ac899ec0
commit
fc8711d4d8
39 files changed
+695
-232
No files matched your search
@@ -136,12 +136,14 @@ window.rustProxyBackendDashboard = (() => {
|
||||
let inPerSecond = samples.inbound.at(-1) ?? 0;
|
||||
let outPerSecond = samples.outbound.at(-1) ?? 0;
|
||||
const additional = { http5xx: 0, timeouts: 0, network: 0, retries: 0, queue: 0 };
|
||||
let fraction = null;
|
||||
let fraction = null, pausedRate = false;
|
||||
if (recordSample) {
|
||||
const now = performance.now();
|
||||
const next = memberCounters(members);
|
||||
const elapsed = previous ? (now - previous.when) / 1000 : 0;
|
||||
if (elapsed > 0) {
|
||||
const valid = rustProxyRateSampling.continuous(previous, now, rustProxyRateSampling.cadence("overview_seconds"));
|
||||
pausedRate = previous !== null && !valid;
|
||||
if (valid) {
|
||||
const elapsed = (now - previous.at) / 1000;
|
||||
const diff = deltas(next, previous.counters);
|
||||
requestsPerSecond = diff.requests / elapsed;
|
||||
errorsPerSecond = diff.errors / elapsed;
|
||||
@@ -150,23 +152,24 @@ window.rustProxyBackendDashboard = (() => {
|
||||
fraction = diff.requests ? 100 * diff.errors / diff.requests : null;
|
||||
for (const name of Object.keys(additional)) additional[name] = diff[name] / elapsed;
|
||||
} else { requestsPerSecond = 0; errorsPerSecond = 0; inPerSecond = 0; outPerSecond = 0; }
|
||||
previous = { when: now, counters: next };
|
||||
previous = { ...rustProxyRateSampling.stamp(now), counters: next };
|
||||
if (visible("backend_activity")) push("active", active);
|
||||
if (visible("backend_requests")) push("requests", requestsPerSecond);
|
||||
if (!pausedRate && visible("backend_requests")) push("requests", requestsPerSecond);
|
||||
if (visible("backend_capacity") && capacityPercent !== null) push("capacity", capacityPercent);
|
||||
if (visible("backend_errors")) push("errors", errorsPerSecond);
|
||||
if (!pausedRate && visible("backend_errors")) push("errors", errorsPerSecond);
|
||||
if (visible("backend_latency") && latency !== null) push("latency", latency);
|
||||
if (visible("backend_traffic")) { push("inbound", inPerSecond); push("outbound", outPerSecond); }
|
||||
for (const name of Object.keys(additional)) if (visible(`backend_${name === "http5xx" ? "http5xx" : name}`)) push(name, additional[name]);
|
||||
if (!pausedRate && visible("backend_traffic")) { push("inbound", inPerSecond); push("outbound", outPerSecond); }
|
||||
if (!pausedRate) for (const name of Object.keys(additional)) if (visible(`backend_${name === "http5xx" ? "http5xx" : name}`)) push(name, additional[name]);
|
||||
}
|
||||
if (!recordSample) for (const name of Object.keys(additional)) additional[name] = samples[name].at(-1) ?? 0;
|
||||
const displayRate = value => pausedRate ? "-" : shortRate(value);
|
||||
if (visible("backend_activity")) {
|
||||
set("backend-dashboard-activity", fmt(active));
|
||||
set("backend-dashboard-activity-detail", t("backendWidgetInFlight"));
|
||||
paintSeries("active", "backend-dashboard-active-line", Math.max(1, ...samples.active));
|
||||
}
|
||||
if (visible("backend_requests")) {
|
||||
set("backend-dashboard-requests", shortRate(requestsPerSecond));
|
||||
set("backend-dashboard-requests", displayRate(requestsPerSecond));
|
||||
set("backend-dashboard-requests-detail", `${t("backendWidgetRequestsTotal")}: ${fmt(totalRequests)}`);
|
||||
paintSeries("requests", "backend-dashboard-request-line", Math.max(1, ...samples.requests));
|
||||
}
|
||||
@@ -178,7 +181,7 @@ window.rustProxyBackendDashboard = (() => {
|
||||
}
|
||||
if (visible("backend_errors")) {
|
||||
set("backend-dashboard-errors", fmt(totalErrors));
|
||||
set("backend-dashboard-errors-detail", `${t("backendWidgetErrorsRate")}: ${shortRate(errorsPerSecond)}${fraction === null ? "" : ` · ${fraction.toFixed(1)}% ${t("backendWidgetErrorPercent")}`}`);
|
||||
set("backend-dashboard-errors-detail", `${t("backendWidgetErrorsRate")}: ${displayRate(errorsPerSecond)}${fraction === null ? "" : ` · ${fraction.toFixed(1)}% ${t("backendWidgetErrorPercent")}`}`);
|
||||
paintSeries("errors", "backend-dashboard-error-line", Math.max(1, ...samples.errors));
|
||||
}
|
||||
if (visible("backend_latency")) {
|
||||
@@ -187,9 +190,9 @@ window.rustProxyBackendDashboard = (() => {
|
||||
paintSeries("latency", "backend-dashboard-latency-line", Math.max(1, ...samples.latency));
|
||||
}
|
||||
if (visible("backend_traffic")) {
|
||||
set("backend-dashboard-traffic", `${bytes(Math.round(inPerSecond + outPerSecond))}/s`);
|
||||
set("backend-dashboard-in-rate", `${bytes(Math.round(inPerSecond))}/s`);
|
||||
set("backend-dashboard-out-rate", `${bytes(Math.round(outPerSecond))}/s`);
|
||||
set("backend-dashboard-traffic", pausedRate ? "-" : `${bytes(Math.round(inPerSecond + outPerSecond))}/s`);
|
||||
set("backend-dashboard-in-rate", pausedRate ? "-" : `${bytes(Math.round(inPerSecond))}/s`);
|
||||
set("backend-dashboard-out-rate", pausedRate ? "-" : `${bytes(Math.round(outPerSecond))}/s`);
|
||||
const peak = Math.max(1, ...samples.inbound, ...samples.outbound);
|
||||
paintSeries("inbound", "backend-dashboard-in-line", peak);
|
||||
paintSeries("outbound", "backend-dashboard-out-line", peak);
|
||||
@@ -207,7 +210,7 @@ window.rustProxyBackendDashboard = (() => {
|
||||
name === "network" ? ["connect_errors_total", "dns_errors_total", "tls_errors_total", "io_errors_total"].reduce((v, key) => v + counter(member[key]), 0) :
|
||||
counter(member.queue_timeouts_total) + counter(member.capacity_rejections_total)), 0);
|
||||
set(field, fmt(total));
|
||||
let detail = `${t("backendWidgetRateDetail")}: ${shortRate(additional[name])}`;
|
||||
let detail = `${t("backendWidgetRateDetail")}: ${displayRate(additional[name])}`;
|
||||
if (name === "retries") detail += ` · ${t("backendWidgetRetriesSuccess")}: ${fmt(members.reduce((n, { member }) => n + counter(member.retry_successes_total), 0))}`;
|
||||
if (name === "queue") detail += ` · ${t("backendWidgetQueueRejected")}: ${fmt(members.reduce((n, { member }) => n + counter(member.capacity_rejections_total), 0))}`;
|
||||
if (name === "network") detail += ` · ${t("backendWidgetNetworkBreakdown")}`;
|
||||
|
||||
+13
-15
@@ -230,18 +230,17 @@ function renderStatus(s, systemSample = false) {
|
||||
const now = performance.now();
|
||||
const transformed = s.transform_metrics || {};
|
||||
const counters = { total: Number(m.requests) || 0, h1: Number(m.http1_requests) || 0, h2: Number(m.http2_requests) || 0, h3: Number(m.http3_requests) || 0, transform: (Number(transformed.applied) || 0) + (Number(transformed.cache_hits) || 0) };
|
||||
const rates = { total: 0, h1: 0, h2: 0, h3: 0, transform: 0 };
|
||||
if (previous) {
|
||||
const elapsed = Math.max(.001, (now - previous.time) / 1000);
|
||||
for (const key of ["total", "h1", "h2", "h3", "transform"]) if (counters[key] >= previous[key]) rates[key] = (counters[key] - previous[key]) / elapsed;
|
||||
const keys = ["total", "h1", "h2", "h3", "transform"]; const rateSample = rustProxyRateSampling.difference(previous, keys.map(key => counters[key]), now, rustProxyRateSampling.cadence("overview_seconds"));
|
||||
const pausedRate = previous !== null && rateSample.deltas === null; previous = rateSample.next;
|
||||
const rates = Object.fromEntries(keys.map((key, i) => [key, rateSample.deltas ? rateSample.deltas[i] / rateSample.seconds : 0]));
|
||||
if (!pausedRate) {
|
||||
samples.push(rates.total); samples = samples.slice(-30);
|
||||
for (const key of ["h1", "h2", "h3"]) { protocolSamples[key].push(rates[key]); protocolSamples[key] = protocolSamples[key].slice(-30); }
|
||||
}
|
||||
previous = { time: now, ...counters };
|
||||
samples.push(rates.total); samples = samples.slice(-30);
|
||||
for (const key of ["h1", "h2", "h3"]) { protocolSamples[key].push(rates[key]); protocolSamples[key] = protocolSamples[key].slice(-30); }
|
||||
websocketProtocolSamples.h1.push(Number(m.websocket_h1_active) || 0); websocketProtocolSamples.h1 = websocketProtocolSamples.h1.slice(-30);
|
||||
websocketProtocolSamples.h2.push(Number(m.websocket_h2_active) || 0); websocketProtocolSamples.h2 = websocketProtocolSamples.h2.slice(-30);
|
||||
websocketProtocolSamples.h3.push(Number(m.websocket_h3_active) || 0); websocketProtocolSamples.h3 = websocketProtocolSamples.h3.slice(-30);
|
||||
transformSamples.push(rates.transform); transformSamples = transformSamples.slice(-30);
|
||||
if (!pausedRate) { transformSamples.push(rates.transform); transformSamples = transformSamples.slice(-30); }
|
||||
const peak = Math.max(1, ...samples, ...transformSamples);
|
||||
const points = values => values.map((n, i) => `${i * 600 / Math.max(1, values.length - 1)},${120 - n / peak * 105}`).join(" ");
|
||||
$("sparkline").setAttribute("points", points(samples));
|
||||
@@ -249,12 +248,11 @@ function renderStatus(s, systemSample = false) {
|
||||
$("sparkline-h2").setAttribute("points", points(protocolSamples.h2));
|
||||
$("sparkline-h3").setAttribute("points", points(protocolSamples.h3));
|
||||
$("sparkline-transform").setAttribute("points", points(transformSamples));
|
||||
$("request-rate").textContent = `${rates.total.toFixed(1)} req/s`;
|
||||
$("rate-total").textContent = `${rates.total.toFixed(1)}/s`; $("rate-h1").textContent = `${rates.h1.toFixed(1)}/s`; $("rate-h2").textContent = `${rates.h2.toFixed(1)}/s`; $("rate-h3").textContent = `${rates.h3.toFixed(1)}/s`; $("rate-transform").textContent = `${rates.transform.toFixed(1)}/s`;
|
||||
$("metric-http-proto-total").textContent = `${rates.total.toFixed(1)}/s`;
|
||||
$("metric-http-proto-h1").textContent = `${rates.h1.toFixed(1)}/s`;
|
||||
$("metric-http-proto-h2").textContent = `${rates.h2.toFixed(1)}/s`;
|
||||
$("metric-http-proto-h3").textContent = `${rates.h3.toFixed(1)}/s`;
|
||||
const rateText = key => pausedRate ? "-" : `${rates[key].toFixed(1)}/s`;
|
||||
$("request-rate").textContent = pausedRate ? "- req/s" : `${rates.total.toFixed(1)} req/s`;
|
||||
$("rate-total").textContent = rateText("total"); $("rate-h1").textContent = rateText("h1"); $("rate-h2").textContent = rateText("h2"); $("rate-h3").textContent = rateText("h3"); $("rate-transform").textContent = rateText("transform");
|
||||
$("metric-http-proto-total").textContent = rateText("total"); $("metric-http-proto-h1").textContent = rateText("h1");
|
||||
$("metric-http-proto-h2").textContent = rateText("h2"); $("metric-http-proto-h3").textContent = rateText("h3");
|
||||
$("http-protocol-h1").setAttribute("points", sparklinePoints(protocolSamples.h1, 300, 60, 5));
|
||||
$("http-protocol-h2").setAttribute("points", sparklinePoints(protocolSamples.h2, 300, 60, 5));
|
||||
$("http-protocol-h3").setAttribute("points", sparklinePoints(protocolSamples.h3, 300, 60, 5));
|
||||
@@ -302,7 +300,7 @@ function stopLive() {
|
||||
}
|
||||
function startLive() {
|
||||
if (!authenticated || document.hidden || typeof EventSource === "undefined") return;
|
||||
const seconds = window.rustProxyRefresh?.current().overview_seconds || 2;
|
||||
const seconds = window.rustProxyRefresh?.current().overview_seconds || 3;
|
||||
if (liveEvents && liveInterval === seconds) return;
|
||||
stopLive();
|
||||
liveInterval = seconds;
|
||||
|
||||
@@ -16,16 +16,15 @@ window.rustProxyExtraOverview = (() => {
|
||||
const node = id => document.querySelector(`[data-dashboard-widget="${id}"]`);
|
||||
function elapsedDelta(id, counters, now) {
|
||||
const old = lastCounter.get(id);
|
||||
const next = { values: counters, at: now };
|
||||
lastCounter.set(id, next);
|
||||
if (!old || now <= old.at || counters.some((v, i) => v < old.values[i])) return null;
|
||||
return { deltas: counters.map((v, i) => v - old.values[i]), seconds: (now - old.at) / 1000 };
|
||||
const result = rustProxyRateSampling.difference(old, counters, now, rustProxyRateSampling.cadence("overview_seconds"));
|
||||
lastCounter.set(id, result.next);
|
||||
return result.deltas ? result : null;
|
||||
}
|
||||
function read(id, status, now, record) {
|
||||
const m = status.metrics || {}, d = status.system || {}, c = status.cache || {};
|
||||
const m = status.metrics || {}, d = status.system || {}, live = status.live_system || d, c = status.cache || {};
|
||||
const direct = {
|
||||
extra_process_cpu: d.process_cpu_percent,
|
||||
extra_process_rss: d.process_rss_bytes,
|
||||
extra_process_cpu: live.process_cpu_percent,
|
||||
extra_process_rss: live.process_rss_bytes,
|
||||
extra_process_fds: d.open_fds,
|
||||
extra_open_connections: m.active_connections,
|
||||
extra_websocket_active: m.websocket_active,
|
||||
|
||||
@@ -7,11 +7,11 @@ window.rustProxyWideOverview = (() => {
|
||||
},
|
||||
wide_cpu: {
|
||||
unit: "percent", names: ["system", "process"], values: s =>
|
||||
[s.system?.cpu_percent, s.system?.process_cpu_percent]
|
||||
[(s.live_system || s.system)?.cpu_percent, (s.live_system || s.system)?.process_cpu_percent]
|
||||
},
|
||||
wide_memory: {
|
||||
unit: "bytes", names: ["process", "cache"], values: s =>
|
||||
[s.system?.process_rss_bytes, s.cache?.resident_bytes]
|
||||
[(s.live_system || s.system)?.process_rss_bytes, s.cache?.resident_bytes]
|
||||
},
|
||||
wide_cache_activity: {
|
||||
unit: "rate", names: ["hits", "misses"], counters: s =>
|
||||
@@ -51,12 +51,9 @@ window.rustProxyWideOverview = (() => {
|
||||
const counters = values.map(finite);
|
||||
if (counters.some(n => n === null)) { baselines.delete(id); return counters.map(() => null); }
|
||||
if (!record) return last.get(id) || counters.map(() => null);
|
||||
const previous = baselines.get(id);
|
||||
baselines.set(id, { counters, now });
|
||||
if (!previous || now <= previous.now || counters.some((n, i) => n < previous.counters[i]))
|
||||
return counters.map(() => null);
|
||||
const elapsed = (now - previous.now) / 1000;
|
||||
return counters.map((n, i) => (n - previous.counters[i]) / elapsed);
|
||||
const result = rustProxyRateSampling.difference(baselines.get(id), counters, now, rustProxyRateSampling.cadence("overview_seconds"));
|
||||
baselines.set(id, result.next);
|
||||
return result.deltas ? result.deltas.map(delta => delta / result.seconds) : counters.map(() => null);
|
||||
}
|
||||
function path(values, peak) {
|
||||
const count = values.length;
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// One continuity policy for all browser-side counter-derived rates.
|
||||
// Background tabs cannot supply a valid instantaneous rate for an interval
|
||||
// spanning the pause. Keep the plotted history and rebase on the next sample.
|
||||
const rustProxyRateSampling = (() => {
|
||||
let generation = 0;
|
||||
const pause = () => { generation += 1; };
|
||||
document.addEventListener?.("visibilitychange", () => { if (document.hidden) pause(); });
|
||||
window.addEventListener?.("pagehide", pause);
|
||||
function cadence(key) {
|
||||
const value = window.rustProxyRefresh?.current?.()?.[key];
|
||||
const fallback = key === "cache_seconds" ? 5 : 3;
|
||||
return Number.isFinite(value) && value > 0 ? value : fallback;
|
||||
}
|
||||
function stamp(at) { return { at, generation }; }
|
||||
function continuous(previous, at, intervalSeconds = 3) {
|
||||
const seconds = Number.isFinite(intervalSeconds) && intervalSeconds > 0 ? intervalSeconds : 3;
|
||||
return Boolean(previous && !document.hidden && previous.generation === generation &&
|
||||
Number.isFinite(at) && Number.isFinite(previous.at) && at > previous.at &&
|
||||
at - previous.at <= Math.max(10000, seconds * 3000));
|
||||
}
|
||||
function difference(previous, values, at, intervalSeconds = 3) {
|
||||
const next = { ...stamp(at), values: [...values] };
|
||||
if (!continuous(previous, at, intervalSeconds) ||
|
||||
values.some((value, i) => !Number.isFinite(value) || value < 0 ||
|
||||
!Number.isFinite(previous.values[i]) || value < previous.values[i])) {
|
||||
return { next, deltas: null, seconds: null };
|
||||
}
|
||||
return { next, deltas: values.map((value, i) => value - previous.values[i]),
|
||||
seconds: (at - previous.at) / 1000 };
|
||||
}
|
||||
return { cadence, stamp, continuous, difference };
|
||||
})();
|
||||
@@ -1,9 +1,9 @@
|
||||
const REFRESH_DEFAULTS = Object.freeze({
|
||||
live_enabled: true, overview_seconds: 2, traffic_seconds: 5,
|
||||
live_enabled: true, overview_seconds: 3, traffic_seconds: 5,
|
||||
cache_seconds: 5, system_info_seconds: 60
|
||||
});
|
||||
const REFRESH_CHOICES = Object.freeze({
|
||||
overview_seconds: [1, 2, 5, 10], traffic_seconds: [0, 3, 5, 10, 30],
|
||||
overview_seconds: [1, 2, 3, 5, 10], traffic_seconds: [0, 3, 5, 10, 30],
|
||||
cache_seconds: [0, 3, 5, 10, 30], system_info_seconds: [0, 15, 30, 60, 300]
|
||||
});
|
||||
function normalizeRefreshPreferences(input = {}) {
|
||||
|
||||
@@ -312,30 +312,30 @@ function renderNetworkInterfaceCharts(interfaces, appendSample) {
|
||||
}
|
||||
}
|
||||
function renderDiagnostics(status, appendSample = true, systemSample = appendSample) {
|
||||
const d = status.system || {}, c = status.cache || {}, m = status.metrics || {};
|
||||
const d = status.system || {}, live = status.live_system || d, c = status.cache || {}, m = status.metrics || {};
|
||||
$("metric-h3").textContent = fmt(m.http3_requests);
|
||||
$("metric-cache-ratio").textContent = c.hit_ratio == null ? "-" : `${fmt(c.hit_ratio * 100, 1)}%`;
|
||||
$("cache-summary").textContent = `${fmt(c.hits)} HIT / ${fmt(c.misses)} MISS`;
|
||||
if (d.process_rss_bytes != null) $("metric-rss").textContent = fmtBytes(d.process_rss_bytes);
|
||||
$("process-summary").hidden = d.process_threads == null;
|
||||
if (d.process_threads != null) $("process-summary").textContent = `${fmt(d.process_threads)} ${t("threads")}`;
|
||||
const systemCpuSupported = d.cpu_percent != null;
|
||||
setOptionalMetric("system-cpu", systemCpuSupported, systemCpuSupported ? `${fmt(d.cpu_percent, 1)}%` : "");
|
||||
$("system-chart").hidden = !systemCpuSupported;
|
||||
const sourceSupported = !!d.source && d.source !== "unsupported";
|
||||
$("system-source").hidden = !sourceSupported;
|
||||
if (sourceSupported) $("system-source").textContent = t("realSample");
|
||||
if (systemSample && d.cpu_percent != null) { systemSamples.push(d.cpu_percent); systemSamples = systemSamples.slice(-30); }
|
||||
// Slow details vs cached live chart telemetry.
|
||||
if (systemSample) {
|
||||
if (d.process_rss_bytes != null) $("metric-rss").textContent = fmtBytes(d.process_rss_bytes);
|
||||
$("process-summary").hidden = d.process_threads == null;
|
||||
if (d.process_threads != null) $("process-summary").textContent = `${fmt(d.process_threads)} ${t("threads")}`;
|
||||
setOptionalMetric("system-cpu", d.cpu_percent != null, `${fmt(d.cpu_percent, 1)}%`);
|
||||
const sourceSupported = !!d.source && d.source !== "unsupported";
|
||||
$("system-source").hidden = !sourceSupported; if (sourceSupported) $("system-source").textContent = t("realSample");
|
||||
const systemMemory = knownPair(d.memory_used_bytes, d.memory_total_bytes, value => fmtBytes(value));
|
||||
setOptionalMetric("system-memory", systemMemory != null, systemMemory);
|
||||
$("memory-meter").hidden = systemMemory == null;
|
||||
if (d.memory_used_bytes != null && d.memory_total_bytes > 0) $("memory-meter").value = d.memory_used_bytes / d.memory_total_bytes * 100; else $("memory-meter").removeAttribute("value");
|
||||
const systemSwap = knownPair(d.system_swap_used_bytes, d.system_swap_total_bytes, value => fmtBytes(value));
|
||||
setOptionalMetric("system-swap", systemSwap != null, systemSwap);
|
||||
const loadSupported = Array.isArray(d.load_average) && d.load_average.length > 0;
|
||||
setOptionalMetric("system-load", loadSupported, loadSupported ? d.load_average.map(n => fmt(n, 2)).join(" / ") : "");
|
||||
setOptionalMetric("system-uptime", d.system_uptime_seconds != null, d.system_uptime_seconds == null ? "" : uptime(d.system_uptime_seconds));
|
||||
}
|
||||
$("system-chart").hidden = live.cpu_percent == null; if (appendSample && live.cpu_percent != null) { systemSamples.push(Number(live.cpu_percent)); systemSamples = systemSamples.slice(-30); }
|
||||
$("system-sparkline").setAttribute("points", systemSamples.map((n, i) => `${i * 300 / Math.max(1, systemSamples.length - 1)},${95 - Math.max(0, Math.min(100, n)) * .9}`).join(" "));
|
||||
const systemMemory = knownPair(d.memory_used_bytes, d.memory_total_bytes, value => fmtBytes(value));
|
||||
setOptionalMetric("system-memory", systemMemory != null, systemMemory);
|
||||
$("memory-meter").hidden = systemMemory == null;
|
||||
if (d.memory_used_bytes != null && d.memory_total_bytes > 0) $("memory-meter").value = d.memory_used_bytes / d.memory_total_bytes * 100; else $("memory-meter").removeAttribute("value");
|
||||
const systemSwap = knownPair(d.system_swap_used_bytes, d.system_swap_total_bytes, value => fmtBytes(value));
|
||||
setOptionalMetric("system-swap", systemSwap != null, systemSwap);
|
||||
const loadSupported = Array.isArray(d.load_average) && d.load_average.length > 0;
|
||||
setOptionalMetric("system-load", loadSupported, loadSupported ? d.load_average.map(n => fmt(n, 2)).join(" / ") : "");
|
||||
setOptionalMetric("system-uptime", d.system_uptime_seconds != null, d.system_uptime_seconds == null ? "" : uptime(d.system_uptime_seconds));
|
||||
$("cache-state").textContent = t(envelope?.config.cache?.enabled ? "enabled" : "notEnabled");
|
||||
$("cache-counters").textContent = [c.hits, c.misses, c.bypasses].map(n => fmt(n)).join(" / ");
|
||||
$("cache-objects").textContent = `${fmt(c.entries)} / ${fmtBytes(c.resident_bytes)}`;
|
||||
@@ -357,15 +357,15 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
if (c.cold_bytes != null) { cacheColdStorageSamples.push(Number(c.cold_bytes)); cacheColdStorageSamples = cacheColdStorageSamples.slice(-60); }
|
||||
const currentCounters = [c.hits, c.misses, c.bypasses].map(Number);
|
||||
if (currentCounters.every(Number.isFinite)) {
|
||||
const delta = lastCacheCounters ? currentCounters.map((value, index) => value >= lastCacheCounters[index] ? value - lastCacheCounters[index] : 0) : [0, 0, 0];
|
||||
cacheHitSamples.push(delta[0]); cacheMissSamples.push(delta[1]); cacheBypassSamples.push(delta[2]);
|
||||
cacheHitSamples = cacheHitSamples.slice(-60); cacheMissSamples = cacheMissSamples.slice(-60); cacheBypassSamples = cacheBypassSamples.slice(-60);
|
||||
lastCacheCounters = currentCounters;
|
||||
const delta = rustProxyRateSampling.difference(lastCacheCounters, currentCounters, performance.now(), rustProxyRateSampling.cadence("overview_seconds"));
|
||||
if (delta.deltas || !lastCacheCounters) { const values = delta.deltas || [0, 0, 0];
|
||||
cacheHitSamples.push(values[0]); cacheMissSamples.push(values[1]); cacheBypassSamples.push(values[2]); cacheHitSamples = cacheHitSamples.slice(-60); cacheMissSamples = cacheMissSamples.slice(-60); cacheBypassSamples = cacheBypassSamples.slice(-60);
|
||||
} lastCacheCounters = delta.next;
|
||||
}
|
||||
}
|
||||
if (systemSample) {
|
||||
if (d.process_cpu_percent != null) { processCpuSamples.push(Number(d.process_cpu_percent)); processCpuSamples = processCpuSamples.slice(-60); }
|
||||
if (d.process_rss_bytes != null) { processRssSamples.push(Number(d.process_rss_bytes)); processRssSamples = processRssSamples.slice(-60); }
|
||||
if (appendSample) {
|
||||
if (live.process_cpu_percent != null) { processCpuSamples.push(Number(live.process_cpu_percent)); processCpuSamples = processCpuSamples.slice(-60); }
|
||||
if (live.process_rss_bytes != null) { processRssSamples.push(Number(live.process_rss_bytes)); processRssSamples = processRssSamples.slice(-60); }
|
||||
}
|
||||
const cacheOutcomePeak = Math.max(1, ...cacheHitSamples, ...cacheMissSamples, ...cacheBypassSamples);
|
||||
$("cache-hit-sparkline").setAttribute("points", sparklinePoints(cacheHitSamples, 300, 60, 5, cacheOutcomePeak));
|
||||
@@ -389,47 +389,49 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
const coldLimit = Number(c.cold_limit_bytes), coldPeak = Number.isFinite(coldLimit) && coldLimit > 0 ? Math.max(coldLimit, ...cacheColdStorageSamples) : null;
|
||||
$("cache-cold-sparkline").setAttribute("points", sparklinePoints(cacheColdStorageSamples, 300, 60, 5, coldPeak));
|
||||
setChartText($("cache-cold-chart-value"), coldEnabled ? occupancyText(c.cold_bytes, c.cold_limit_bytes) : t("notEnabled"));
|
||||
const processPlatform = [d.platform, d.environment].filter(Boolean).join(" / ");
|
||||
setOptionalMetric("process-platform", !!d.platform && d.platform !== "Unsupported", processPlatform);
|
||||
setOptionalMetric("process-cpu", d.process_cpu_percent != null, `${fmt(d.process_cpu_percent, 1)}%`);
|
||||
const fds = knownPair(d.open_fds, d.max_fds, value => fmt(value), t("unlimited"));
|
||||
setOptionalMetric("process-fds", fds != null, fds);
|
||||
setOptionalMetric("process-threads", d.process_threads != null, fmt(d.process_threads));
|
||||
const virtualMemory = knownPair(d.process_virtual_memory_bytes, d.process_virtual_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
|
||||
setOptionalMetric("process-virtual-memory", virtualMemory != null, virtualMemory);
|
||||
setOptionalMetric("process-start-time", d.process_start_time_seconds != null, d.process_start_time_seconds == null ? "" : formatDateTime(Number(d.process_start_time_seconds) * 1000));
|
||||
setOptionalMetric("process-uptime", status.uptime_seconds != null, status.uptime_seconds == null ? "" : uptime(status.uptime_seconds));
|
||||
setOptionalMetric("process-rss-total", d.process_rss_bytes != null, fmtBytes(d.process_rss_bytes));
|
||||
const memory = status.memory_accounting || {};
|
||||
const hotMemory = knownPair(memory.cache_hot_bytes, memory.cache_hot_limit_bytes, value => fmtBytes(value));
|
||||
const transientMemory = knownPair(memory.cache_transient_bytes, memory.cache_transient_limit_bytes, value => fmtBytes(value));
|
||||
const transformMemory = knownPair(memory.transform_reserved_bytes, memory.transform_budget_bytes, value => fmtBytes(value));
|
||||
const transformInflight = knownPair(memory.transform_inflight, memory.transform_max_inflight, value => fmt(value));
|
||||
setOptionalMetric("memory-cache-hot", hotMemory != null, hotMemory);
|
||||
setOptionalMetric("memory-cache-transient", transientMemory != null, transientMemory);
|
||||
setOptionalMetric("memory-transform", transformMemory != null, transformMemory);
|
||||
setOptionalMetric("memory-transform-inflight", transformInflight != null, transformInflight);
|
||||
setOptionalMetric("memory-non-cache-rss", memory.non_cache_rss_bytes != null, fmtBytes(memory.non_cache_rss_bytes));
|
||||
setOptionalMetric("process-rss-anon", d.process_rss_anon_bytes != null, fmtBytes(d.process_rss_anon_bytes));
|
||||
const rssFile = knownPair(d.process_rss_file_bytes, d.process_rss_shmem_bytes, value => fmtBytes(value));
|
||||
setOptionalMetric("process-rss-file", rssFile != null, rssFile);
|
||||
const dataSwap = knownPair(d.process_data_bytes, d.process_swap_bytes, value => fmtBytes(value));
|
||||
setOptionalMetric("process-data-swap", dataSwap != null, dataSwap);
|
||||
const cgroupMemory = knownPair(d.cgroup_memory_current_bytes, d.cgroup_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
|
||||
setOptionalMetric("cgroup-memory", cgroupMemory != null, cgroupMemory);
|
||||
if (systemSample) {
|
||||
const processPlatform = [d.platform, d.environment].filter(Boolean).join(" / ");
|
||||
setOptionalMetric("process-platform", !!d.platform && d.platform !== "Unsupported", processPlatform);
|
||||
setOptionalMetric("process-cpu", d.process_cpu_percent != null, `${fmt(d.process_cpu_percent, 1)}%`);
|
||||
const fds = knownPair(d.open_fds, d.max_fds, value => fmt(value), t("unlimited"));
|
||||
setOptionalMetric("process-fds", fds != null, fds);
|
||||
setOptionalMetric("process-threads", d.process_threads != null, fmt(d.process_threads));
|
||||
const virtualMemory = knownPair(d.process_virtual_memory_bytes, d.process_virtual_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
|
||||
setOptionalMetric("process-virtual-memory", virtualMemory != null, virtualMemory);
|
||||
setOptionalMetric("process-start-time", d.process_start_time_seconds != null, d.process_start_time_seconds == null ? "" : formatDateTime(Number(d.process_start_time_seconds) * 1000));
|
||||
setOptionalMetric("process-uptime", status.uptime_seconds != null, status.uptime_seconds == null ? "" : uptime(status.uptime_seconds));
|
||||
setOptionalMetric("process-rss-total", d.process_rss_bytes != null, fmtBytes(d.process_rss_bytes));
|
||||
const memory = status.memory_accounting || {};
|
||||
const hotMemory = knownPair(memory.cache_hot_bytes, memory.cache_hot_limit_bytes, value => fmtBytes(value));
|
||||
const transientMemory = knownPair(memory.cache_transient_bytes, memory.cache_transient_limit_bytes, value => fmtBytes(value));
|
||||
const transformMemory = knownPair(memory.transform_reserved_bytes, memory.transform_budget_bytes, value => fmtBytes(value));
|
||||
const transformInflight = knownPair(memory.transform_inflight, memory.transform_max_inflight, value => fmt(value));
|
||||
setOptionalMetric("memory-cache-hot", hotMemory != null, hotMemory);
|
||||
setOptionalMetric("memory-cache-transient", transientMemory != null, transientMemory);
|
||||
setOptionalMetric("memory-transform", transformMemory != null, transformMemory);
|
||||
setOptionalMetric("memory-transform-inflight", transformInflight != null, transformInflight);
|
||||
setOptionalMetric("memory-non-cache-rss", memory.non_cache_rss_bytes != null, fmtBytes(memory.non_cache_rss_bytes));
|
||||
setOptionalMetric("process-rss-anon", d.process_rss_anon_bytes != null, fmtBytes(d.process_rss_anon_bytes));
|
||||
const rssFile = knownPair(d.process_rss_file_bytes, d.process_rss_shmem_bytes, value => fmtBytes(value));
|
||||
setOptionalMetric("process-rss-file", rssFile != null, rssFile);
|
||||
const dataSwap = knownPair(d.process_data_bytes, d.process_swap_bytes, value => fmtBytes(value));
|
||||
setOptionalMetric("process-data-swap", dataSwap != null, dataSwap);
|
||||
const cgroupMemory = knownPair(d.cgroup_memory_current_bytes, d.cgroup_memory_max_bytes, value => fmtBytes(value), t("unlimited"));
|
||||
setOptionalMetric("cgroup-memory", cgroupMemory != null, cgroupMemory);
|
||||
}
|
||||
$("process-cpu-sparkline").setAttribute("points", sparklinePoints(processCpuSamples));
|
||||
setChartText($("process-cpu-chart-value"), d.process_cpu_percent == null ? "" : `${fmt(d.process_cpu_percent, 1)}%`);
|
||||
setChartText($("process-cpu-chart-value"), live.process_cpu_percent == null ? "" : `${fmt(live.process_cpu_percent, 1)}%`);
|
||||
$("process-rss-sparkline").setAttribute("points", sparklinePoints(processRssSamples));
|
||||
setChartText($("process-rss-chart-value"), d.process_rss_bytes == null ? "" : fmtBytes(d.process_rss_bytes));
|
||||
$("process-cpu-sparkline").closest(".mini-live-chart").hidden = d.process_cpu_percent == null;
|
||||
$("process-rss-sparkline").closest(".mini-live-chart").hidden = d.process_rss_bytes == null;
|
||||
setChartText($("process-rss-chart-value"), live.process_rss_bytes == null ? "" : fmtBytes(live.process_rss_bytes));
|
||||
$("process-cpu-sparkline").closest(".mini-live-chart").hidden = live.process_cpu_percent == null;
|
||||
$("process-rss-sparkline").closest(".mini-live-chart").hidden = live.process_rss_bytes == null;
|
||||
updateCacheMemorySizing();
|
||||
const networkRateSupported = d.network_rx_bytes_per_second != null && d.network_tx_bytes_per_second != null;
|
||||
setOptionalMetric("system-network", networkRateSupported, networkRateSupported ? `${fmtBytes(d.network_rx_bytes_per_second)} / ${fmtBytes(d.network_tx_bytes_per_second)}` : "");
|
||||
const networkRateSupported = live.network_rx_bytes_per_second != null && live.network_tx_bytes_per_second != null;
|
||||
if (systemSample) setOptionalMetric("system-network", d.network_rx_bytes_per_second != null && d.network_tx_bytes_per_second != null, `${fmtBytes(d.network_rx_bytes_per_second)} / ${fmtBytes(d.network_tx_bytes_per_second)}`);
|
||||
setOptionalMetric("mean-latency", m.mean_duration_ms != null, m.mean_duration_ms == null ? "" : `${fmt(m.mean_duration_ms, 2)} ms`);
|
||||
if (appendSample) {
|
||||
if (d.network_rx_bytes_per_second != null && d.network_tx_bytes_per_second != null) {
|
||||
networkRxSamples.push(Number(d.network_rx_bytes_per_second)); networkTxSamples.push(Number(d.network_tx_bytes_per_second));
|
||||
if (networkRateSupported) {
|
||||
networkRxSamples.push(Number(live.network_rx_bytes_per_second)); networkTxSamples.push(Number(live.network_tx_bytes_per_second));
|
||||
networkRxSamples = networkRxSamples.slice(-60); networkTxSamples = networkTxSamples.slice(-60);
|
||||
}
|
||||
if (m.mean_duration_ms != null) { responseLatencySamples.push(Number(m.mean_duration_ms)); responseLatencySamples = responseLatencySamples.slice(-60); }
|
||||
@@ -437,12 +439,10 @@ function renderDiagnostics(status, appendSample = true, systemSample = appendSam
|
||||
const networkPeak = Math.max(1, ...networkRxSamples, ...networkTxSamples);
|
||||
$("network-rx-sparkline").setAttribute("points", sparklinePoints(networkRxSamples, 300, 60, 5, networkPeak));
|
||||
$("network-tx-sparkline").setAttribute("points", sparklinePoints(networkTxSamples, 300, 60, 5, networkPeak));
|
||||
setChartText($("network-chart-value"), networkRateSupported ? `${fmtBytes(d.network_rx_bytes_per_second)} / ${fmtBytes(d.network_tx_bytes_per_second)}` : "");
|
||||
setChartText($("network-chart-value"), networkRateSupported ? `${fmtBytes(live.network_rx_bytes_per_second)} / ${fmtBytes(live.network_tx_bytes_per_second)}` : "");
|
||||
$("network-rx-sparkline").closest(".mini-live-chart").hidden = !networkRateSupported;
|
||||
// Advance interface charts at the live Overview cadence, just like the
|
||||
// aggregate network chart. Between slower system-info reads, repeat the
|
||||
// latest observed rates without initiating another system data fetch.
|
||||
renderNetworkInterfaceCharts(d.network_interfaces || [], appendSample);
|
||||
// Cache-sampled OS chart rates are independent from slow System details.
|
||||
renderNetworkInterfaceCharts(live.network_interfaces || [], appendSample);
|
||||
$("latency-sparkline").setAttribute("points", sparklinePoints(responseLatencySamples));
|
||||
setChartText($("latency-chart-value"), m.mean_duration_ms == null ? "" : `${fmt(m.mean_duration_ms, 2)} ms`);
|
||||
$("latency-sparkline").closest(".mini-live-chart").hidden = m.mean_duration_ms == null;
|
||||
|
||||
@@ -88,17 +88,17 @@ window.rustProxyStreamDashboard = (() => {
|
||||
const rxTotal = views.reduce((sum, view) => sum + (Number(view.bytes_in) || 0), 0);
|
||||
const txTotal = views.reduce((sum, view) => sum + (Number(view.bytes_out) || 0), 0);
|
||||
const now = performance.now();
|
||||
let rx = 0, tx = 0;
|
||||
if (history.last) {
|
||||
const seconds = Math.max(.001, (now - history.last.when) / 1000);
|
||||
if (rxTotal >= history.last.rx) rx = (rxTotal - history.last.rx) / seconds;
|
||||
if (txTotal >= history.last.tx) tx = (txTotal - history.last.tx) / seconds;
|
||||
const sample = rustProxyRateSampling.difference(history.last, [rxTotal, txTotal], now, rustProxyRateSampling.cadence("overview_seconds"));
|
||||
const pausedRate = history.last !== null && sample.deltas === null;
|
||||
history.last = sample.next;
|
||||
const rx = sample.deltas ? sample.deltas[0] / sample.seconds : 0;
|
||||
const tx = sample.deltas ? sample.deltas[1] / sample.seconds : 0;
|
||||
if (!pausedRate) {
|
||||
history.rx.push(rx); history.tx.push(tx);
|
||||
if (history.rx.length > 60) { history.rx.shift(); history.tx.shift(); }
|
||||
}
|
||||
history.last = { when: now, rx: rxTotal, tx: txTotal };
|
||||
history.rx.push(rx); history.tx.push(tx);
|
||||
if (history.rx.length > 60) { history.rx.shift(); history.tx.shift(); }
|
||||
set("stream-dashboard-traffic", `${bytes(Math.round(rx))}/s`);
|
||||
trafficRates(`${bytes(Math.round(rx))}/s`, `${bytes(Math.round(tx))}/s`);
|
||||
set("stream-dashboard-traffic", pausedRate ? "-" : `${bytes(Math.round(rx))}/s`);
|
||||
trafficRates(pausedRate ? "-" : `${bytes(Math.round(rx))}/s`, pausedRate ? "-" : `${bytes(Math.round(tx))}/s`);
|
||||
$("stream-dashboard-rx")?.setAttribute("points", line(history.rx));
|
||||
$("stream-dashboard-tx")?.setAttribute("points", line(history.tx));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let loading = false, offset = 0, generation = 0;
|
||||
let liveChartScope = "", liveChartPrevious = null, liveChartPreviousAt = null;
|
||||
let liveChartScope = "", liveChartPrevious = null, liveChartPreviousAt = null, liveChartGeneration = null;
|
||||
const liveChartSamples = { hits: [], misses: [], bypasses: [] };
|
||||
const LIVE_CHART_LIMITS = new Set([60, 120, 300, 600]);
|
||||
const LIVE_CHART_STORAGE_KEY = "rustproxy.cache.liveChartSamples";
|
||||
@@ -68,7 +68,7 @@
|
||||
};
|
||||
const reason = value => { const key = `cacheReason_${value}`, text = t(key); return text === key ? value : text; };
|
||||
function resetLiveChart(selected = scope()) {
|
||||
liveChartScope = selected; liveChartPrevious = null; liveChartPreviousAt = null;
|
||||
liveChartScope = selected; liveChartPrevious = null; liveChartPreviousAt = null; liveChartGeneration = null;
|
||||
for (const values of Object.values(liveChartSamples)) values.length = 0;
|
||||
for (const id of ["cache-live-hit-line", "cache-live-miss-line", "cache-live-bypass-line"]) $(id)?.setAttribute("points", "0,170 900,170");
|
||||
for (const id of ["cache-live-hit-rate", "cache-live-miss-rate", "cache-live-bypass-rate"]) if ($(id)) $(id).textContent = "0.0/s";
|
||||
@@ -100,18 +100,21 @@
|
||||
hits: Math.max(0, Number(counters.hits) || 0),
|
||||
misses: Math.max(0, Number(counters.misses) || 0),
|
||||
bypasses: Math.max(0, Number(counters.bypasses) || 0)
|
||||
}, rates = { hits: 0, misses: 0, bypasses: 0 };
|
||||
if (liveChartPrevious && liveChartPreviousAt != null) {
|
||||
const elapsed = Math.max(.001, (now - liveChartPreviousAt) / 1000);
|
||||
for (const key of Object.keys(rates)) if (current[key] >= liveChartPrevious[key]) rates[key] = (current[key] - liveChartPrevious[key]) / elapsed;
|
||||
};
|
||||
const keys = Object.keys(current);
|
||||
const previous = liveChartPrevious && liveChartPreviousAt != null
|
||||
? { values: keys.map(key => liveChartPrevious[key]), at: liveChartPreviousAt, generation: liveChartGeneration } : null;
|
||||
const sample = rustProxyRateSampling.difference(previous, keys.map(key => current[key]), now, rustProxyRateSampling.cadence("cache_seconds"));
|
||||
liveChartPrevious = current; liveChartPreviousAt = now; liveChartGeneration = sample.next.generation;
|
||||
if (sample.deltas) {
|
||||
keys.forEach((key, i) => { liveChartSamples[key].push(sample.deltas[i] / sample.seconds); liveChartSamples[key] = liveChartSamples[key].slice(-liveChartLimit); });
|
||||
}
|
||||
liveChartPrevious = current; liveChartPreviousAt = now;
|
||||
for (const key of Object.keys(rates)) { liveChartSamples[key].push(rates[key]); liveChartSamples[key] = liveChartSamples[key].slice(-liveChartLimit); }
|
||||
redrawLiveChart();
|
||||
$("cache-live-hit-rate").textContent = `${rates.hits.toFixed(1)}/s`;
|
||||
$("cache-live-miss-rate").textContent = `${rates.misses.toFixed(1)}/s`;
|
||||
$("cache-live-bypass-rate").textContent = `${rates.bypasses.toFixed(1)}/s`;
|
||||
$("cache-live-chart-value").textContent = `Σ ${(rates.hits + rates.misses + rates.bypasses).toFixed(1)}/s`;
|
||||
const rate = key => sample.deltas ? `${(sample.deltas[keys.indexOf(key)] / sample.seconds).toFixed(1)}/s` : "-";
|
||||
$("cache-live-hit-rate").textContent = rate("hits");
|
||||
$("cache-live-miss-rate").textContent = rate("misses");
|
||||
$("cache-live-bypass-rate").textContent = rate("bypasses");
|
||||
$("cache-live-chart-value").textContent = sample.deltas ? `Σ ${(sample.deltas.reduce((a, b) => a + b, 0) / sample.seconds).toFixed(1)}/s` : "-";
|
||||
}
|
||||
function configure() {
|
||||
const selected = scope(); const items = [el("option", t("logAllVhosts"))]; items[0].value = "*";
|
||||
|
||||
@@ -2520,7 +2520,8 @@
|
||||
<select id="refresh-overview-seconds"
|
||||
data-refresh-preference="overview_seconds">
|
||||
<option value="1">1 s</option>
|
||||
<option selected value="2">2 s</option>
|
||||
<option value="2">2 s</option>
|
||||
<option selected value="3">3 s</option>
|
||||
<option value="5">5 s</option>
|
||||
<option value="10">10 s</option>
|
||||
</select></label>
|
||||
|
||||
@@ -250,14 +250,17 @@
|
||||
const set = (suffix, value) => document.querySelectorAll(`[data-${suffix.replace(/[A-Z]/g, c => `-${c.toLowerCase()}`)}="${CSS.escape(id)}"]`).forEach(node => node.textContent = value);
|
||||
set("streamState", state); set("streamActive", data ? fmt(data.active) : "-"); set("streamSessions", data ? fmt(data.sessions_total) : "-"); set("streamErrors", data ? fmt(data.errors) : "-"); set("streamRejected", data ? fmt(data.rejected) : "-"); set("streamTransferred", data ? bytes((Number(data.bytes_in) || 0) + (Number(data.bytes_out) || 0)) : "-");
|
||||
if (!data || currentPanel !== "streams") continue;
|
||||
const history = chartHistory.get(id) || { lastTime: now, lastIn: Number(data.bytes_in) || 0, lastOut: Number(data.bytes_out) || 0, points: [], bucketSize: 1, pendingRx: 0, pendingTx: 0, pendingCount: 0 };
|
||||
const elapsed = Math.max(.001, (now - history.lastTime) / 1000), inBytes = Number(data.bytes_in) || 0, outBytes = Number(data.bytes_out) || 0;
|
||||
const rx = inBytes >= history.lastIn ? (inBytes - history.lastIn) / elapsed : 0, tx = outBytes >= history.lastOut ? (outBytes - history.lastOut) / elapsed : 0;
|
||||
history.lastTime = now; history.lastIn = inBytes; history.lastOut = outBytes; addPoint(history, rx, tx); chartHistory.set(id, history);
|
||||
const history = chartHistory.get(id) || { last: null, points: [], bucketSize: 1, pendingRx: 0, pendingTx: 0, pendingCount: 0 };
|
||||
const sample = rustProxyRateSampling.difference(history.last, [Number(data.bytes_in) || 0, Number(data.bytes_out) || 0], now, rustProxyRateSampling.cadence("overview_seconds"));
|
||||
const pausedRate = history.last !== null && sample.deltas === null;
|
||||
history.last = sample.next;
|
||||
const rx = sample.deltas ? sample.deltas[0] / sample.seconds : 0, tx = sample.deltas ? sample.deltas[1] / sample.seconds : 0;
|
||||
if (!pausedRate) addPoint(history, rx, tx);
|
||||
chartHistory.set(id, history);
|
||||
const points = history.pendingCount ? [...history.points, [history.pendingRx / history.pendingCount, history.pendingTx / history.pendingCount]] : history.points;
|
||||
const peak = Math.max(1, ...points.flat()); const toPoints = index => points.map((pair, pointIndex) => `${pointIndex * 320 / Math.max(1, points.length - 1)},${50 - pair[index] / peak * 44}`).join(" ");
|
||||
const svg = document.querySelector(`[data-stream-chart="${CSS.escape(id)}"]`); svg?.querySelector(".stream-chart-rx")?.setAttribute("points", toPoints(0)); svg?.querySelector(".stream-chart-tx")?.setAttribute("points", toPoints(1));
|
||||
const label = document.querySelector(`[data-stream-chart-value="${CSS.escape(id)}"]`); if (label) label.textContent = `RX ${bytes(Math.round(rx))}/s · TX ${bytes(Math.round(tx))}/s`;
|
||||
const label = document.querySelector(`[data-stream-chart-value="${CSS.escape(id)}"]`); if (label) label.textContent = pausedRate ? "RX - · TX -" : `RX ${bytes(Math.round(rx))}/s · TX ${bytes(Math.round(tx))}/s`;
|
||||
}
|
||||
if (editing) { const data = metrics[editing]; $("stream-live-summary").textContent = data ? `${t("streamActiveSessions")}: ${fmt(data.active)} · RX ${bytes(data.bytes_in)} · TX ${bytes(data.bytes_out)} · ${t("streamErrors")}: ${fmt(data.errors)}` : t("streamNoLiveMetrics"); }
|
||||
}
|
||||
|
||||
+13
-3
@@ -10713,13 +10713,13 @@ html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner
|
||||
.dashboard-widget-setting, .overview-layout-tile, .overview-layout-hidden-item,
|
||||
.overview-inline-hidden-item, .overview-inline-hidden, .notice, .toast,
|
||||
.chart-style-choice, .stream-dashboard-card, .pool-card, .vhost-card,
|
||||
.vhost-backend-protocol-badge, .vh-backend-card, .entity, .settings-filter-row,
|
||||
.vh-backend-card, .entity, .settings-filter-row,
|
||||
.stream-backend-row,
|
||||
.file-log-advanced-parser, .table-container, .metric-info-popover
|
||||
) { border-radius: var(--rp-radius-surface) !important; }
|
||||
html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner-style="rounded"], [data-corner-style="extra"]) :is(
|
||||
button:not(.nav):not(.file-log-tab), input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
|
||||
select, textarea, .pill, .overview-widget-type, .badge, .quiet, .outline, .primary,
|
||||
select, textarea, .pill, .vhost-backend-protocol-badge, .overview-widget-type, .badge, .quiet, .outline, .primary,
|
||||
.code, .command, .dashboard-widget-toggle, .settings-tabs button,
|
||||
.overview-inline-hidden-toggle, .overview-inline-restore
|
||||
) { border-radius: var(--rp-radius-control) !important; }
|
||||
@@ -10743,7 +10743,6 @@ html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner
|
||||
border: 1px solid var(--pill-line);
|
||||
background: var(--panel2);
|
||||
color: var(--muted);
|
||||
border-radius: 4px;
|
||||
padding: 3px 7px;
|
||||
font: 10px var(--mono);
|
||||
overflow-wrap: anywhere;
|
||||
@@ -10752,3 +10751,14 @@ html:is([data-corner-style="square"], [data-corner-style="subtle"], [data-corner
|
||||
color: var(--green);
|
||||
border-color: var(--pill-live-line);
|
||||
}
|
||||
|
||||
/* Phone Overview has one vertical scroll surface. Nested overflow on the
|
||||
interface chart stack can steal a WebKit touch/momentum gesture and appear
|
||||
to jump back to Process / Network. Desktop retains its bounded list. */
|
||||
@container overview-canvas (max-width: 700px) {
|
||||
#panel-overview .network-interface-charts {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
overscroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
(() => {
|
||||
let loading = false, generation = 0, lastData = null;
|
||||
let liveChartScope = "", liveChartPrevious = null, liveChartPreviousAt = null;
|
||||
let liveChartScope = "", liveChartPrevious = null, liveChartPreviousAt = null, liveChartGeneration = null;
|
||||
const liveChartSamples = { applied: [], cache_hits: [], skipped: [], errors: [] };
|
||||
const LIVE_CHART_LIMITS = new Set([60, 120, 300, 600]);
|
||||
const LIVE_CHART_STORAGE_KEY = "rustproxy.transform.liveChartSamples";
|
||||
@@ -20,7 +20,7 @@
|
||||
};
|
||||
const emptyCounters = () => ({ examined: 0, applied: 0, skipped: 0, errors: 0, cache_hits: 0, original_bytes: 0, transformed_bytes: 0, saved_bytes: 0, duration_us: 0, by_kind: {}, reasons: {} });
|
||||
function resetLiveChart(selected = scope()) {
|
||||
liveChartScope = selected; liveChartPrevious = null; liveChartPreviousAt = null;
|
||||
liveChartScope = selected; liveChartPrevious = null; liveChartPreviousAt = null; liveChartGeneration = null;
|
||||
for (const values of Object.values(liveChartSamples)) values.length = 0;
|
||||
for (const id of ["transform-live-applied-line", "transform-live-cache-line", "transform-live-skipped-line", "transform-live-error-line"]) $(id)?.setAttribute("points", "0,170 900,170");
|
||||
for (const id of ["transform-live-applied-rate", "transform-live-cache-rate", "transform-live-skipped-rate", "transform-live-error-rate"]) if ($(id)) $(id).textContent = "0.0/s";
|
||||
@@ -54,19 +54,22 @@
|
||||
cache_hits: Math.max(0, Number(counters.cache_hits) || 0),
|
||||
skipped: Math.max(0, Number(counters.skipped) || 0),
|
||||
errors: Math.max(0, Number(counters.errors) || 0)
|
||||
}, rates = { applied: 0, cache_hits: 0, skipped: 0, errors: 0 };
|
||||
if (liveChartPrevious && liveChartPreviousAt != null) {
|
||||
const elapsed = Math.max(.001, (now - liveChartPreviousAt) / 1000);
|
||||
for (const key of Object.keys(rates)) if (current[key] >= liveChartPrevious[key]) rates[key] = (current[key] - liveChartPrevious[key]) / elapsed;
|
||||
};
|
||||
const keys = Object.keys(current);
|
||||
const previous = liveChartPrevious && liveChartPreviousAt != null
|
||||
? { values: keys.map(key => liveChartPrevious[key]), at: liveChartPreviousAt, generation: liveChartGeneration } : null;
|
||||
const sample = rustProxyRateSampling.difference(previous, keys.map(key => current[key]), now, 3);
|
||||
liveChartPrevious = current; liveChartPreviousAt = now; liveChartGeneration = sample.next.generation;
|
||||
if (sample.deltas) {
|
||||
keys.forEach((key, i) => { liveChartSamples[key].push(sample.deltas[i] / sample.seconds); liveChartSamples[key] = liveChartSamples[key].slice(-liveChartLimit); });
|
||||
}
|
||||
liveChartPrevious = current; liveChartPreviousAt = now;
|
||||
for (const key of Object.keys(rates)) { liveChartSamples[key].push(rates[key]); liveChartSamples[key] = liveChartSamples[key].slice(-liveChartLimit); }
|
||||
redrawLiveChart();
|
||||
$("transform-live-applied-rate").textContent = `${rates.applied.toFixed(1)}/s`;
|
||||
$("transform-live-cache-rate").textContent = `${rates.cache_hits.toFixed(1)}/s`;
|
||||
$("transform-live-skipped-rate").textContent = `${rates.skipped.toFixed(1)}/s`;
|
||||
$("transform-live-error-rate").textContent = `${rates.errors.toFixed(1)}/s`;
|
||||
$("transform-live-chart-value").textContent = `Σ ${(rates.applied + rates.cache_hits + rates.skipped + rates.errors).toFixed(1)}/s`;
|
||||
const rate = key => sample.deltas ? `${(sample.deltas[keys.indexOf(key)] / sample.seconds).toFixed(1)}/s` : "-";
|
||||
$("transform-live-applied-rate").textContent = rate("applied");
|
||||
$("transform-live-cache-rate").textContent = rate("cache_hits");
|
||||
$("transform-live-skipped-rate").textContent = rate("skipped");
|
||||
$("transform-live-error-rate").textContent = rate("errors");
|
||||
$("transform-live-chart-value").textContent = sample.deltas ? `Σ ${(sample.deltas.reduce((a, b) => a + b, 0) / sample.seconds).toFixed(1)}/s` : "-";
|
||||
}
|
||||
function countersFor(data) {
|
||||
if (scope() === "*") return data?.metrics?.total || emptyCounters();
|
||||
|
||||
@@ -264,10 +264,10 @@ function backendTrafficRate(poolId, member, snapshot, now, seen) {
|
||||
const reported = Math.max(0, Number(member.traffic_bytes_per_second) || 0);
|
||||
const previous = backendTrafficHistory.get(key);
|
||||
if (previous?.snapshot === snapshot) return previous.rate;
|
||||
const elapsed = previous ? Math.max(.001, (now - previous.at) / 1000) : 0;
|
||||
const observed = previous && total >= previous.total ? (total - previous.total) / elapsed : 0;
|
||||
const continuous = rustProxyRateSampling.continuous(previous, now, rustProxyRateSampling.cadence("overview_seconds"));
|
||||
const observed = continuous && total >= previous.total ? (total - previous.total) / ((now - previous.at) / 1000) : 0;
|
||||
const rate = reported > 0 ? reported : observed;
|
||||
backendTrafficHistory.set(key, { total, at: now, snapshot, rate });
|
||||
backendTrafficHistory.set(key, { ...rustProxyRateSampling.stamp(now), total, snapshot, rate });
|
||||
return rate;
|
||||
}
|
||||
function vhostTrafficRate(id, metrics, snapshot, now, seen) {
|
||||
@@ -275,9 +275,9 @@ function vhostTrafficRate(id, metrics, snapshot, now, seen) {
|
||||
const total = Math.max(0, Number(metrics?.bytes_in) || 0) + Math.max(0, Number(metrics?.bytes_out) || 0);
|
||||
const previous = vhostTrafficHistory.get(id);
|
||||
if (previous?.snapshot === snapshot) return previous.rate;
|
||||
const elapsed = previous ? Math.max(.001, (now - previous.at) / 1000) : 0;
|
||||
const rate = previous && total >= previous.total ? (total - previous.total) / elapsed : 0;
|
||||
vhostTrafficHistory.set(id, { total, at: now, snapshot, rate });
|
||||
const continuous = rustProxyRateSampling.continuous(previous, now, rustProxyRateSampling.cadence("overview_seconds"));
|
||||
const rate = continuous && total >= previous.total ? (total - previous.total) / ((now - previous.at) / 1000) : 0;
|
||||
vhostTrafficHistory.set(id, { ...rustProxyRateSampling.stamp(now), total, snapshot, rate });
|
||||
return rate;
|
||||
}
|
||||
function renderStatus(status) {
|
||||
@@ -357,14 +357,19 @@ function renderStatus(status) {
|
||||
fill("vhostLatency", d => d.latency > 0 ? `${fmt(d.latency, 1)} ms` : "-"); fill("vhostTraffic", d => `${rateBytes(d.traffic)}/s`);
|
||||
const now = performance.now();
|
||||
for (const [id, data] of aggregate) {
|
||||
const history = chartHistory.get(id) || { lastTime: now, lastRequests: data.requests, points: [], bucketSize: 1, pendingSum: 0, pendingCount: 0 };
|
||||
const elapsed = Math.max(.001, (now - history.lastTime) / 1000); const rate = data.requests >= history.lastRequests ? (data.requests - history.lastRequests) / elapsed : 0; history.lastTime = now; history.lastRequests = data.requests;
|
||||
history.pendingSum += rate; history.pendingCount += 1;
|
||||
if (history.pendingCount >= history.bucketSize) { history.points.push(history.pendingSum / history.pendingCount); history.pendingSum = 0; history.pendingCount = 0; }
|
||||
const history = chartHistory.get(id) || { last: null, points: [], bucketSize: 1, pendingSum: 0, pendingCount: 0 };
|
||||
const sample = rustProxyRateSampling.difference(history.last, [data.requests], now, rustProxyRateSampling.cadence("overview_seconds"));
|
||||
const pausedRate = history.last !== null && sample.deltas === null;
|
||||
history.last = sample.next;
|
||||
const rate = sample.deltas ? sample.deltas[0] / sample.seconds : 0;
|
||||
if (!pausedRate) {
|
||||
history.pendingSum += rate; history.pendingCount += 1;
|
||||
if (history.pendingCount >= history.bucketSize) { history.points.push(history.pendingSum / history.pendingCount); history.pendingSum = 0; history.pendingCount = 0; }
|
||||
}
|
||||
if (history.points.length >= 120) { const compacted = []; for (let i = 0; i < history.points.length; i += 2) compacted.push((history.points[i] + history.points[i + 1]) / 2); history.points = compacted; history.bucketSize *= 2; }
|
||||
chartHistory.set(id, history);
|
||||
const displayPoints = history.pendingCount ? [...history.points, history.pendingSum / history.pendingCount] : history.points; const actualPeak = Math.max(0, ...displayPoints), scalePeak = Math.max(1, actualPeak); const points = displayPoints.map((value, index) => `${index * 320 / Math.max(1, displayPoints.length - 1)},${50 - value / scalePeak * 44}`).join(" ");
|
||||
const svg = document.querySelector(`[data-chart-for="${CSS.escape(id)}"]`); svg?.querySelector("polyline")?.setAttribute("points", points); const label = document.querySelector(`[data-chart-value-for="${CSS.escape(id)}"]`); if (label) label.textContent = `${rate.toFixed(1)} ${t("vhReqPerSecond")} · ${t("vhPeak")} ${actualPeak.toFixed(1)}`;
|
||||
const svg = document.querySelector(`[data-chart-for="${CSS.escape(id)}"]`); svg?.querySelector("polyline")?.setAttribute("points", points); const label = document.querySelector(`[data-chart-value-for="${CSS.escape(id)}"]`); if (label) label.textContent = `${pausedRate ? "-" : rate.toFixed(1)} ${t("vhReqPerSecond")} · ${t("vhPeak")} ${actualPeak.toFixed(1)}`;
|
||||
}
|
||||
const editingVhost = editing ? configuredVhosts.get(editing) : null;
|
||||
const editingHasProxy = editingVhost && (editingVhost.source?.type !== "web_server" || (editingVhost.routes || []).some(route => !route.response && (route.source?.type || "reverse_proxy") === "reverse_proxy"));
|
||||
|
||||
Reference in new issue
Block a user