This commit is contained in:
Mateusz Gruszczyński committed 2026-10-03 10:36:30 +02:00
1 parent 579d9c806b
commit 1297ea2920
34 files changed
+320 -96

No files matched your search

+30 -4
View File
@@ -506,7 +506,7 @@ $("reload-certificates").addEventListener("click", async () => {
$("reset-certificate").addEventListener("click", () => $("certificate-form").reset());
// Dashboard metrics are actual server samples. Null means unavailable, not zero.
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null,
let systemSamples = [], connectionOpenSamples = [], connectionExchangeSamples = [], cacheMemorySamples = [], cacheTransientSamples = [], cacheFillSamples = [], cacheColdQueueSamples = [], cacheColdIoSamples = [], cacheColdStorageSamples = [], cacheHitSamples = [], cacheMissSamples = [], cacheBypassSamples = [], lastCacheCounters = null,
processCpuSamples = [], processRssSamples = [], networkRxSamples = [], networkTxSamples = [], responseLatencySamples = [], networkInterfaceSamples = new Map(), settingsDirty = new Set();
const hiddenNetworkInterfaces = (() => {
try {
@@ -545,6 +545,12 @@ function knownPair(left, right, format, zeroRight = null) {
const rightText = right === 0 && zeroRight ? zeroRight : format(right);
return `${format(left)} / ${rightText}`;
}
function occupancyText(used, limit) {
if (used == null) return "-";
const usedNumber = Number(used), limitNumber = Number(limit);
if (!Number.isFinite(limitNumber) || limitNumber <= 0) return fmtBytes(usedNumber);
return `${fmtBytes(usedNumber)} / ${fmtBytes(limitNumber)} · ${fmt(usedNumber / limitNumber * 100, 1)}%`;
}
function sparklinePoints(values, width = 300, height = 60, padding = 5, peakOverride = null) {
const peak = Math.max(1, peakOverride ?? Math.max(0, ...values));
return values.map((value, index) => `${index * width / Math.max(1, values.length - 1)},${height - padding - Math.max(0, value) / peak * (height - padding * 2)}`).join(" ");
@@ -642,6 +648,12 @@ function renderDiagnostics(status, appendSample = true) {
$("cache-evictions").textContent = `${fmt(c.evictions)} / ${fmt(c.expired)}`;
if (appendSample) {
if (c.resident_bytes != null) { cacheMemorySamples.push(Number(c.resident_bytes)); cacheMemorySamples = cacheMemorySamples.slice(-60); }
if (c.transient_bytes != null) { cacheTransientSamples.push(Number(c.transient_bytes)); cacheTransientSamples = cacheTransientSamples.slice(-60); }
if (c.fill_bytes_active != null) { cacheFillSamples.push(Number(c.fill_bytes_active)); cacheFillSamples = cacheFillSamples.slice(-60); }
if (c.cold_queue_bytes != null) { cacheColdQueueSamples.push(Number(c.cold_queue_bytes)); cacheColdQueueSamples = cacheColdQueueSamples.slice(-60); }
const coldIoBytes = (Number(c.cold_write_bytes_active) || 0) + (Number(c.cold_read_bytes_active) || 0);
if (c.cold_write_bytes_active != null || c.cold_read_bytes_active != null) { cacheColdIoSamples.push(coldIoBytes); cacheColdIoSamples = cacheColdIoSamples.slice(-60); }
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];
@@ -658,8 +670,22 @@ function renderDiagnostics(status, appendSample = true) {
$("cache-bypass-sparkline").setAttribute("points", sparklinePoints(cacheBypassSamples, 300, 60, 5, cacheOutcomePeak));
const latestCacheDelta = [cacheHitSamples.at(-1), cacheMissSamples.at(-1), cacheBypassSamples.at(-1)];
$("cache-outcome-chart-value").textContent = latestCacheDelta.every(value => value != null) ? latestCacheDelta.map(value => fmt(value)).join(" / ") : "-";
$("cache-memory-sparkline").setAttribute("points", sparklinePoints(cacheMemorySamples));
$("cache-chart-value").textContent = fmtBytes(c.resident_bytes);
const hotLimit = Number(c.hot_limit_bytes), hotPeak = Number.isFinite(hotLimit) && hotLimit > 0 ? Math.max(hotLimit, ...cacheMemorySamples) : null;
$("cache-memory-sparkline").setAttribute("points", sparklinePoints(cacheMemorySamples, 300, 60, 5, hotPeak));
$("cache-chart-value").textContent = occupancyText(c.resident_bytes, c.hot_limit_bytes);
const transientLimit = Number(c.transient_limit_bytes);
const transientPeak = Number.isFinite(transientLimit) && transientLimit > 0
? Math.max(transientLimit, ...cacheTransientSamples, ...cacheFillSamples, ...cacheColdQueueSamples, ...cacheColdIoSamples)
: Math.max(1, ...cacheTransientSamples, ...cacheFillSamples, ...cacheColdQueueSamples, ...cacheColdIoSamples);
$("cache-transient-sparkline").setAttribute("points", sparklinePoints(cacheTransientSamples, 300, 60, 5, transientPeak));
$("cache-fill-sparkline").setAttribute("points", sparklinePoints(cacheFillSamples, 300, 60, 5, transientPeak));
$("cache-queue-sparkline").setAttribute("points", sparklinePoints(cacheColdQueueSamples, 300, 60, 5, transientPeak));
$("cache-io-sparkline").setAttribute("points", sparklinePoints(cacheColdIoSamples, 300, 60, 5, transientPeak));
$("cache-transient-chart-value").textContent = occupancyText(c.transient_bytes, c.transient_limit_bytes);
const coldEnabled = envelope?.config.cache?.cold_enabled === true;
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));
$("cache-cold-chart-value").textContent = coldEnabled ? occupancyText(c.cold_bytes, c.cold_limit_bytes) : t("notEnabled");
setOptionalMetric("process-platform", !!d.platform && d.platform !== "Unsupported", d.platform || "");
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"));
@@ -752,7 +778,7 @@ function renderSettings() {
if (!settingsDirty.has("response-form")) {
const server = config.server_header || {}, compression = config.compression || {}, diagnostics = config.diagnostic_headers || {};
$("expose-request-id").checked = !!diagnostics.request_id;
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.24.7";
$("server-mode").value = server.mode || "preserve"; $("server-value").value = server.value || "RustProxy/v0.24.8";
for (const name of ["gzip", "br", "zstd", "deflate"]) $(`compress-${name}`).checked = !!compression[name];
$("compress-min").value = compression.min_bytes ?? 1024; syncServerMode();
}
+48 -10
View File
@@ -294,12 +294,43 @@
class="cache-bypass-key">BYPASS</span></div>
</div>
<div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="cacheResidentHistory">Resident
RAM history</span><b id="cache-chart-value">-</b></div>
<svg aria-label="RAM cache resident bytes history"
data-i18n-aria="cacheResidentHistory" role="img" viewbox="0 0 300 60">
<div class="mini-live-chart-head"><span data-i18n="cacheHotMemoryHistory">HOT RAM
usage / limit</span><b id="cache-chart-value">-</b></div>
<svg aria-label="HOT cache RAM usage history"
data-i18n-aria="cacheHotMemoryHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line" id="cache-memory-sparkline"
<polyline class="diagnostic-line cache-hot-line" id="cache-memory-sparkline"
points="0,55 300,55"></polyline>
</svg>
</div>
<div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="cacheTransientMemoryHistory">Transient
RAM usage / limit</span><b id="cache-transient-chart-value">-</b></div>
<svg aria-label="Transient cache RAM usage history"
data-i18n-aria="cacheTransientMemoryHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line cache-transient-total-line" id="cache-transient-sparkline"
points="0,55 300,55"></polyline>
<polyline class="diagnostic-line cache-fill-line" id="cache-fill-sparkline"
points="0,55 300,55"></polyline>
<polyline class="diagnostic-line cache-queue-line" id="cache-queue-sparkline"
points="0,55 300,55"></polyline>
<polyline class="diagnostic-line cache-io-line" id="cache-io-sparkline"
points="0,55 300,55"></polyline>
</svg>
<div class="mini-chart-legend" aria-hidden="true"><span
class="cache-transient-total-key" data-i18n="cacheTransientShort">TOTAL</span><span
class="cache-fill-key" data-i18n="cacheFillShort">fill</span><span
class="cache-queue-key" data-i18n="cacheQueueShort">cold queue</span><span
class="cache-io-key" data-i18n="cacheIoShort">cold I/O</span></div>
</div>
<div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="cacheColdStorageHistory">COLD storage
usage / limit</span><b id="cache-cold-chart-value">-</b></div>
<svg aria-label="COLD storage usage history"
data-i18n-aria="cacheColdStorageHistory" role="img" viewbox="0 0 300 60">
<path class="chart-grid" d="M0 15H300 M0 30H300 M0 45H300"></path>
<polyline class="diagnostic-line cache-cold-line" id="cache-cold-sparkline"
points="0,55 300,55"></polyline>
</svg>
</div>
@@ -307,8 +338,16 @@
</article>
<article class="card">
<div class="mini-heading">
<h3 data-i18n="processDiagnostics">Process</h3><span class="meta"
id="system-sample-age">-</span>
<h3 data-i18n="processDiagnostics">Process</h3>
<div class="mini-heading-actions">
<details class="metric-info">
<summary data-i18n-aria="memoryAccountingInfo" aria-label="Memory accounting information">i</summary>
<div class="metric-info-popover">
<p class="fine" data-i18n="memoryAccountingHint">RSS is the real process footprint. HOT/transient cache values are RustProxy-accounted allocations; transform memory is a conservative active reservation. Non-cache RSS is RSS minus accounted cache and includes transforms, connections, TLS, logging, Tokio/runtime state, allocator overhead and other buffers, so these rows are not all additive.</p>
</div>
</details>
<span class="meta" id="system-sample-age">-</span>
</div>
</div>
<dl class="mini-metrics">
<div>
@@ -380,7 +419,6 @@
<dd id="cgroup-memory">-</dd>
</div>
</dl>
<p class="fine" data-i18n="memoryAccountingHint">RSS is the real process footprint. HOT/transient cache values are RustProxy-accounted allocations; transform memory is a conservative active reservation. Non-cache RSS is RSS minus accounted cache and includes transforms, connections, TLS, logging, Tokio/runtime state, allocator overhead and other buffers, so these rows are not all additive.</p>
<div class="mini-live-chart-stack">
<div class="mini-live-chart">
<div class="mini-live-chart-head"><span data-i18n="processCpuHistory">Process CPU
@@ -1353,7 +1391,7 @@
<option data-i18n="serverCustom" value="custom">Replace / add custom value</option>
</select>
<label data-i18n="serverValue" for="server-value">Custom Server value</label><input
id="server-value" maxlength="256" placeholder="RustProxy/v0.24.7"
id="server-value" maxlength="256" placeholder="RustProxy/v0.24.8"
spellcheck="false" />
<h3 class="subheading" data-i18n="diagnosticHeaders">Diagnostic headers</h3>
<div class="switch-grid">
@@ -2060,7 +2098,7 @@
</section>
</div>
</section>
<footer class="page-footer"><span>RustProxy <b>0.24.7</b></span><span id="footer-status">API - /
<footer class="page-footer"><span>RustProxy <b>0.24.8</b></span><span id="footer-status">API - /
dropped: 0</span></footer>
</div>
</main>
+92 -1
View File
@@ -7351,4 +7351,95 @@ body[data-density="compact"] .page-footer {
#cache-memory-warning {
color: var(--notice-text);
font-weight: 600;
}
}
/* 0.24.8 cache occupancy charts and compact memory-accounting help */
.mini-heading-actions {
display: inline-flex;
align-items: center;
justify-content: flex-end;
gap: 7px;
min-width: 0;
}
.metric-info {
position: relative;
}
.metric-info>summary {
width: 18px;
height: 18px;
display: inline-grid;
place-items: center;
list-style: none;
cursor: pointer;
border: 1px solid var(--line);
border-radius: 50%;
color: var(--muted);
background: var(--panel2);
font: 700 10px var(--mono);
user-select: none;
}
.metric-info>summary::-webkit-details-marker {
display: none;
}
.metric-info>summary:hover,
.metric-info>summary:focus-visible,
.metric-info[open]>summary {
color: var(--text);
border-color: var(--border-hover);
outline: none;
}
.metric-info-popover {
position: absolute;
z-index: 40;
top: calc(100% + 7px);
right: 0;
width: min(360px, calc(100vw - 44px));
padding: 10px 11px;
border: 1px solid var(--line);
border-radius: 6px;
background: var(--panel2);
box-shadow: 0 12px 30px var(--shadow-mid);
}
.metric-info-popover .fine {
margin: 0;
line-height: 1.5;
}
.cache-hot-line,
.cache-transient-total-line {
stroke: var(--accent);
}
.cache-fill-line {
stroke: var(--green);
}
.cache-queue-line,
.cache-cold-line {
stroke: var(--blue);
}
.cache-io-line {
stroke: var(--transform-chart);
}
.cache-transient-total-key {
color: var(--accent);
}
.cache-fill-key {
color: var(--green);
}
.cache-queue-key {
color: var(--blue);
}
.cache-io-key {
color: var(--transform-chart);
}
+1 -1
View File
@@ -600,7 +600,7 @@
authMode.addEventListener("change", syncAuth); syncAuth();
g = group("vhResponsePolicy"); const server = value.server_header, diagnostics = value.diagnostic_headers;
const serverMode = field(g, "server-mode", "serverHeader", server?.mode || "inherit", "select", { values: [["inherit", "vhInherit"], ["preserve", "serverPreserve"], ["hide", "serverHide"], ["custom", "serverCustom"]] });
const serverValue = field(g, "server-value", "serverValue", server?.value || "RustProxy/v0.24.7", "text", { maxLength: 256 });
const serverValue = field(g, "server-value", "serverValue", server?.value || "RustProxy/v0.24.8", "text", { maxLength: 256 });
const syncServer = () => { serverValue.disabled = serverMode.value !== "custom"; serverValue.required = !serverValue.disabled; }; serverMode.addEventListener("change", syncServer); syncServer();
const inheritDiagnostics = field(g, "diag-inherit", "diagInherit", !diagnostics, "checkbox"), diagInputs = [];
for (const [name, key] of [["request_id", "diagRequestId"]]) diagInputs.push(field(g, `diag-${name}`, key, diagnostics?.[name] ?? envelope?.config.diagnostic_headers?.[name], "checkbox"));