v0.33.3_hotfix
This commit is contained in:
1 parent
78ad28ef4d
commit
de090d20fa
7 files changed
+157
-6
No files matched your search
@@ -5,6 +5,13 @@
|
|||||||
|
|
||||||
## 0.33.3
|
## 0.33.3
|
||||||
|
|
||||||
|
- Avoid one-point holes in full-width Overview counter-rate histories and
|
||||||
|
WWW rate charts after tab suspension, BFCache resume or overdue refresh.
|
||||||
|
Preserve already plotted history, skip unavailable rate samples and display
|
||||||
|
"-" until the next valid interval; genuine measured 0/s remains visible.
|
||||||
|
The Web Server service-rate sparkline no longer draws a false zero dip.
|
||||||
|
Add deterministic JavaScript and simulated Chromium regression coverage.
|
||||||
|
|
||||||
- Make main sidebar navigation highlight depend on the Appearance **interface
|
- Make main sidebar navigation highlight depend on the Appearance **interface
|
||||||
theme** rather than Chart style. Selected/hover backgrounds reuse theme nav
|
theme** rather than Chart style. Selected/hover backgrounds reuse theme nav
|
||||||
tokens; active text and the selection marker use readable theme colors.
|
tokens; active text and the selection marker use readable theme colors.
|
||||||
|
|||||||
@@ -162,8 +162,11 @@ streaming HTTP/TLS/WebSocket forwarding, raw TCP/UDP stream hosts, IP-bound vhos
|
|||||||
Overview/protocols, optional wide/small charts, backend and Stream/Vhost
|
Overview/protocols, optional wide/small charts, backend and Stream/Vhost
|
||||||
traffic charts. After tab hide/pagehide (including BFCache) or a delayed
|
traffic charts. After tab hide/pagehide (including BFCache) or a delayed
|
||||||
refresh, keep plotted history, use the first reading as a new baseline,
|
refresh, keep plotted history, use the first reading as a new baseline,
|
||||||
and resume rate points on the next normal interval. A genuine measured
|
and resume rate points on the next normal interval. Full-width Overview,
|
||||||
zero still appears as 0/s. Cache and Transform intervals and the separate
|
WWW and Web Server charts keep their drawn lines continuous by not appending
|
||||||
|
the unavailable first rate sample; the current rate displays "-" until a
|
||||||
|
valid interval is measured. A genuine measured zero still appears as 0/s.
|
||||||
|
Cache and Transform intervals and the separate
|
||||||
System information interval remain configurable; no additional polling.
|
System information interval remain configurable; no additional polling.
|
||||||
|
|
||||||
- **Development fixes :** keep Network diagnostics
|
- **Development fixes :** keep Network diagnostics
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// Simulated DOM/status regression: no holes or artificial zeros after tab resume.
|
||||||
|
// This intentionally does not exercise the running Rust server or real browser.
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import vm from 'node:vm';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
|
||||||
|
const code = file => fs.readFileSync(path.join(root, 'src/admin/web', file), 'utf8');
|
||||||
|
const html = code('index.html');
|
||||||
|
|
||||||
|
class Element {
|
||||||
|
constructor() { this.attributes = new Map(); this.style = {}; this.textContent = ''; this.children = []; this.hidden = false; }
|
||||||
|
getAttribute(key) { return this.attributes.get(key) ?? null; }
|
||||||
|
setAttribute(key, value) { this.attributes.set(key, String(value)); }
|
||||||
|
removeAttribute(key) { this.attributes.delete(key); }
|
||||||
|
append(...children) { this.children.push(...children); }
|
||||||
|
replaceChildren(...children) { this.children = children; }
|
||||||
|
}
|
||||||
|
const ids = new Map([...html.matchAll(/\bid="([^"]+)"/g)].map(([, id]) => [id, new Element()]));
|
||||||
|
const widgets = new Map([...html.matchAll(/data-dashboard-widget="([^"]+)"/g)].map(([, id]) => [id, new Element()]));
|
||||||
|
const listeners = new Map();
|
||||||
|
const document = {
|
||||||
|
hidden: false,
|
||||||
|
addEventListener: (type, handler) => listeners.set(type, handler),
|
||||||
|
querySelector: selector => widgets.get(selector.match(/^\[data-dashboard-widget="([^"]+)"\]$/)?.[1]) || null,
|
||||||
|
createElement: () => new Element(),
|
||||||
|
};
|
||||||
|
const samples = {
|
||||||
|
getLimit: () => 60,
|
||||||
|
trim: values => { if (values.length > 60) values.splice(0, values.length - 60); },
|
||||||
|
onChange: () => {},
|
||||||
|
};
|
||||||
|
const window = {
|
||||||
|
addEventListener() {}, rustProxyOverviewSamples: samples,
|
||||||
|
rustProxyRefresh: { current: () => ({ overview_seconds: 3 }) },
|
||||||
|
};
|
||||||
|
let now = 1000;
|
||||||
|
const context = vm.createContext({
|
||||||
|
window, document, performance: { now: () => now }, currentPanel: 'overview', latestStatus: null,
|
||||||
|
$: id => ids.get(id) || null, fmt: (value, n = 0) => Number(value).toFixed(n),
|
||||||
|
fmtBytes: value => `${value} B`, t: value => value,
|
||||||
|
});
|
||||||
|
for (const file of ['app/rate-sampling.js', 'app/overview-wide-metrics.js', 'app/web-overview.js'])
|
||||||
|
vm.runInContext(code(file), context, { filename: file });
|
||||||
|
|
||||||
|
const web = {
|
||||||
|
requests: 200, status_2xx: 190, status_3xx: 2, status_4xx: 5, status_5xx: 3,
|
||||||
|
static_requests: 100, php_requests: 80, other_requests: 20,
|
||||||
|
methods: [160, 20, 10, 5, 5], selected_status: [0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
|
response_headers_us: 300000, response_bytes: 1200000, active_headers: 2,
|
||||||
|
};
|
||||||
|
const php = {
|
||||||
|
metrics: { requests: 80, response_headers_us: 100000, response_bytes: 320000, failures: Array(9).fill(0) },
|
||||||
|
capacity_limit: 6, capacity_used: 1,
|
||||||
|
};
|
||||||
|
const status = {
|
||||||
|
revision: 4,
|
||||||
|
metrics: { requests: 200, http1_requests: 120, http2_requests: 60, http3_requests: 20,
|
||||||
|
compression_gzip: 10, compression_br: 20, compression_zstd: 5, compression_deflate: 8 },
|
||||||
|
cache: { hits: 50, misses: 30 },
|
||||||
|
transform_metrics: { applied: 10, cache_hits: 5, skipped: 1, errors: 0 },
|
||||||
|
web_server_metrics: { demo: { web, php } },
|
||||||
|
};
|
||||||
|
function advance(delta) {
|
||||||
|
for (const key of Object.keys(status.metrics)) status.metrics[key] += delta;
|
||||||
|
for (const key of ['hits', 'misses']) status.cache[key] += delta;
|
||||||
|
for (const key of Object.keys(status.transform_metrics)) status.transform_metrics[key] += delta;
|
||||||
|
for (const key of ['requests', 'status_2xx', 'static_requests', 'php_requests', 'response_headers_us', 'response_bytes']) web[key] += delta;
|
||||||
|
for (const key of ['requests', 'response_headers_us', 'response_bytes']) php.metrics[key] += delta;
|
||||||
|
}
|
||||||
|
function render() { window.rustProxyWideOverview.render(status); window.rustProxyWebOverview.render(status); }
|
||||||
|
const line = id => ids.get(id).getAttribute('d') || '';
|
||||||
|
const compression = 'overview-wide-wide_compression_activity-gzip-line';
|
||||||
|
const website = 'www_requests-all-line';
|
||||||
|
const phpLine = 'www_php_requests-line';
|
||||||
|
const starts = path => (path.match(/M/g) || []).length;
|
||||||
|
|
||||||
|
render(); // no first-interval rate: establish baseline only
|
||||||
|
now = 4000; advance(12); render();
|
||||||
|
const beforeWide = line(compression), beforeWeb = line(website);
|
||||||
|
const beforePhp = ids.get(phpLine).getAttribute('points');
|
||||||
|
assert.equal(starts(beforeWide), 1);
|
||||||
|
assert.equal(starts(beforeWeb), 1);
|
||||||
|
assert.ok(beforePhp);
|
||||||
|
|
||||||
|
// Background tab -> a new generation, regardless of counter increments.
|
||||||
|
document.hidden = true; listeners.get('visibilitychange')();
|
||||||
|
document.hidden = false;
|
||||||
|
now = 100000; advance(600); render();
|
||||||
|
assert.equal(ids.get('overview-wide-wide_compression_activity-summary').textContent, '-');
|
||||||
|
assert.equal(ids.get('www_requests-value').textContent, '-');
|
||||||
|
assert.equal(line(compression), beforeWide, 'wide rate chart must not receive a null point');
|
||||||
|
assert.equal(line(website), beforeWeb, 'WWW rate chart must not receive a null point');
|
||||||
|
assert.equal(ids.get(phpLine).getAttribute('points'), beforePhp, 'small PHP chart must preserve valid history');
|
||||||
|
|
||||||
|
// A fresh interval then connects to previous valid samples without a gap.
|
||||||
|
now = 103000; advance(9); render();
|
||||||
|
assert.equal(starts(line(compression)), 1, 'wide chart must have one connected path');
|
||||||
|
assert.equal(starts(line(website)), 1, 'WWW chart must have one connected path');
|
||||||
|
assert.ok(line(compression).includes('L'));
|
||||||
|
assert.ok(line(website).includes('L'));
|
||||||
|
|
||||||
|
// Real measured zeros must continue to be plotted, not treated as pauses.
|
||||||
|
now = 106000; render();
|
||||||
|
assert.equal(ids.get('overview-wide-wide_compression_activity-summary').textContent, '0.0/s');
|
||||||
|
assert.equal(ids.get('www_requests-value').textContent, '0.0/s');
|
||||||
|
assert.equal(starts(line(compression)), 1);
|
||||||
|
assert.equal(starts(line(website)), 1);
|
||||||
|
|
||||||
|
assert.match(code('web-server.js'), /if \(record && rate !== null\) \{ previous\.push\(rate\)/,
|
||||||
|
'Web Server chart must also skip unavailable rates instead of plotting a zero dip');
|
||||||
|
console.log('PASS: resumed wide/WWW/PHP charts stay connected; first invalid rate is unavailable; measured zero persists');
|
||||||
@@ -116,6 +116,24 @@ with sync_playwright() as p:
|
|||||||
}""", second)
|
}""", second)
|
||||||
assert not details.get('missing') and details['rows'] == 7, (width, details)
|
assert not details.get('missing') and details['rows'] == 7, (width, details)
|
||||||
assert '18 / 32' in details['text'], (width, details)
|
assert '18 / 32' in details['text'], (width, details)
|
||||||
|
# The first reading after an overdue browser interval must establish a
|
||||||
|
# fresh baseline without adding a fake 0/s dip to the service chart.
|
||||||
|
continuity = page.evaluate("""([first, second, paused, resumed]) => {
|
||||||
|
const api = window.rustProxyWebServer;
|
||||||
|
const chart = document.querySelector('#web-service-list [data-web-live-id="site-A"] polyline');
|
||||||
|
const rate = document.querySelector('#web-service-list [data-web-live-field="rate"]');
|
||||||
|
window.__clock = 5000; api.renderLive(first);
|
||||||
|
window.__clock = 8000; api.renderLive(second);
|
||||||
|
const before = chart.getAttribute('points');
|
||||||
|
window.__clock = 100000; api.renderLive(paused);
|
||||||
|
const during = chart.getAttribute('points'), unavailable = rate.textContent;
|
||||||
|
window.__clock = 103000; api.renderLive(resumed);
|
||||||
|
return { before, during, unavailable, after: chart.getAttribute('points'), resumedRate: rate.textContent };
|
||||||
|
}""", [observation(2), observation(3), observation(10), observation(11)])
|
||||||
|
assert continuity['before'] == continuity['during'], (width, continuity)
|
||||||
|
assert continuity['unavailable'] == '-', (width, continuity)
|
||||||
|
assert continuity['resumedRate'] != '-', (width, continuity)
|
||||||
|
assert continuity['after'] != continuity['before'], (width, continuity)
|
||||||
assert not errors, (width, errors)
|
assert not errors, (width, errors)
|
||||||
old = page.evaluate("""() => {
|
old = page.evaluate("""() => {
|
||||||
currentPanel = 'overview';
|
currentPanel = 'overview';
|
||||||
|
|||||||
@@ -90,11 +90,16 @@ window.rustProxyWideOverview = (() => {
|
|||||||
: config.values(status).map(finite);
|
: config.values(status).map(finite);
|
||||||
const series = history.get(id) || config.names.map(() => []);
|
const series = history.get(id) || config.names.map(() => []);
|
||||||
if (record) {
|
if (record) {
|
||||||
|
// Counter rates have no meaningful value on the first sample after a
|
||||||
|
// paused tab. Do not add null to the path: it would split a live line.
|
||||||
|
// Still show "-" above the chart until a fresh interval is sampled.
|
||||||
|
if (!config.counters || values.every(value => value !== null)) {
|
||||||
values.forEach((value, i) => {
|
values.forEach((value, i) => {
|
||||||
series[i].push(value);
|
series[i].push(value);
|
||||||
if (series[i].length > limit()) series[i].shift();
|
if (series[i].length > limit()) series[i].shift();
|
||||||
});
|
});
|
||||||
history.set(id, series);
|
history.set(id, series);
|
||||||
|
}
|
||||||
last.set(id, values);
|
last.set(id, values);
|
||||||
}
|
}
|
||||||
const peak = Math.max(config.unit === "percent" ? 100 : 1,
|
const peak = Math.max(config.unit === "percent" ? 100 : 1,
|
||||||
|
|||||||
@@ -154,9 +154,9 @@ window.rustProxyWebOverview = (() => {
|
|||||||
meter("www_error_rate", r?.errorRate);
|
meter("www_error_rate", r?.errorRate);
|
||||||
show("www_error_rate-detail", t("webMetricErrorRateHelp"));
|
show("www_error_rate-detail", t("webMetricErrorRateHelp"));
|
||||||
show("www_php_requests-value", fmtRate(r?.phpRps));
|
show("www_php_requests-value", fmtRate(r?.phpRps));
|
||||||
if (record) {
|
if (record && r) {
|
||||||
const values = histories.get("www_php_requests") || [];
|
const values = histories.get("www_php_requests") || [];
|
||||||
values.push(r?.phpRps ?? null); window.rustProxyOverviewSamples.trim(values);
|
values.push(r.phpRps); window.rustProxyOverviewSamples.trim(values);
|
||||||
histories.set("www_php_requests", values);
|
histories.set("www_php_requests", values);
|
||||||
}
|
}
|
||||||
$(`www_php_requests-line`)?.setAttribute("points", smallPath(histories.get("www_php_requests") || []));
|
$(`www_php_requests-line`)?.setAttribute("points", smallPath(histories.get("www_php_requests") || []));
|
||||||
@@ -166,7 +166,10 @@ window.rustProxyWebOverview = (() => {
|
|||||||
};
|
};
|
||||||
for (const [id, names] of Object.entries(seriesNames)) {
|
for (const [id, names] of Object.entries(seriesNames)) {
|
||||||
const series = histories.get(id) || names.map(() => []);
|
const series = histories.get(id) || names.map(() => []);
|
||||||
if (record) {
|
// A resumed/overdue interval has no rate; skipping that entire
|
||||||
|
// snapshot keeps existing paths connected without inventing 0/s.
|
||||||
|
// A genuinely unavailable series inside a valid interval stays null.
|
||||||
|
if (record && r) {
|
||||||
names.forEach((_, i) => { series[i].push(set[id]?.[i] ?? null); window.rustProxyOverviewSamples.trim(series[i]); });
|
names.forEach((_, i) => { series[i].push(set[id]?.[i] ?? null); window.rustProxyOverviewSamples.trim(series[i]); });
|
||||||
histories.set(id, series);
|
histories.set(id, series);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -439,7 +439,8 @@
|
|||||||
const deltas = record ? sample.deltas : null;
|
const deltas = record ? sample.deltas : null;
|
||||||
const rate = deltas ? deltas[0] / sample.seconds : null;
|
const rate = deltas ? deltas[0] / sample.seconds : null;
|
||||||
const previous = serviceHistory.get(id) || [];
|
const previous = serviceHistory.get(id) || [];
|
||||||
if (record) { previous.push(rate); window.rustProxyOverviewSamples.trim(previous); serviceHistory.set(id, previous); }
|
// An unavailable counter delta is not a 0/s measurement; do not plot it.
|
||||||
|
if (record && rate !== null) { previous.push(rate); window.rustProxyOverviewSamples.trim(previous); serviceHistory.set(id, previous); }
|
||||||
const peak = Math.max(1, ...previous.filter(x => x != null));
|
const peak = Math.max(1, ...previous.filter(x => x != null));
|
||||||
const points = previous.map((value, i) => `${300 * i / Math.max(1, previous.length - 1)},${55 - (value || 0) / peak * 50}`).join(" ");
|
const points = previous.map((value, i) => `${300 * i / Math.max(1, previous.length - 1)},${55 - (value || 0) / peak * 50}`).join(" ");
|
||||||
panel.querySelector("polyline")?.setAttribute("points", points || "0,55 300,55");
|
panel.querySelector("polyline")?.setAttribute("points", points || "0,55 300,55");
|
||||||
|
|||||||
Reference in new issue
Block a user