v0.33.6
This commit is contained in:
1 parent
5dd802ae83
commit
a286355b0e
22 files changed
+447
-70
No files matched your search
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
"""0.33.5 fixed 0-100% CPU plot and height regression with production HTML/CSS/JS.
|
||||
|
||||
Uses Chromium with simulated authenticated DOM and status samples, not a live
|
||||
RustProxy server, SSE stream or real CPU telemetry.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import re
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HTML = (ROOT / 'src/admin/web/index.html').read_text(encoding='utf-8')
|
||||
CSS = (ROOT / 'src/admin/web/style.css').read_text(encoding='utf-8')
|
||||
SCALE_JS = (ROOT / 'src/admin/web/app/chart-scales.js').read_text(encoding='utf-8')
|
||||
WIDE_JS = (ROOT / 'src/admin/web/app/overview-wide-metrics.js').read_text(encoding='utf-8')
|
||||
|
||||
|
||||
def inspect(page):
|
||||
return page.evaluate('''() => {
|
||||
const get = id => document.querySelector(`#dashboard-overview-grid [data-dashboard-widget="${id}"]`);
|
||||
const cpu = get('wide_cpu'), cpuSvg = cpu.querySelector('svg');
|
||||
const protocolSvg = get('wide_protocol_rates').querySelector('svg');
|
||||
const connection = get('connection_chart');
|
||||
const backendSvg = get('backend_traffic').querySelector('svg');
|
||||
const legend = cpu.querySelector('.overview-wide-legend').getBoundingClientRect();
|
||||
const bounds = cpu.getBoundingClientRect();
|
||||
const matrix = cpuSvg.getScreenCTM();
|
||||
return {
|
||||
cpuHeight: cpuSvg.getBoundingClientRect().height,
|
||||
cpuCardHeight: bounds.height,
|
||||
connectionCardHeight: connection.getBoundingClientRect().height,
|
||||
protocolHeight: protocolSvg.getBoundingClientRect().height,
|
||||
backendHeight: backendSvg.getBoundingClientRect().height,
|
||||
svgWidth: cpuSvg.getBoundingClientRect().width,
|
||||
drawnWidth: Math.hypot(matrix.a, matrix.b) * cpuSvg.viewBox.baseVal.width,
|
||||
ratio: cpuSvg.getAttribute('preserveAspectRatio'),
|
||||
cpuSeries: cpuSvg.querySelectorAll('.overview-wide-series').length,
|
||||
lineEffect: getComputedStyle(cpuSvg.querySelector('.overview-wide-series')).vectorEffect,
|
||||
gridEffect: getComputedStyle(cpuSvg.querySelector('.chart-grid')).vectorEffect,
|
||||
gridPath: cpuSvg.querySelector('.chart-grid').getAttribute('d'),
|
||||
legendInCard: legend.top >= bounds.top && legend.bottom <= bounds.bottom + 1,
|
||||
overflow: document.documentElement.scrollWidth - innerWidth,
|
||||
};
|
||||
}''')
|
||||
|
||||
|
||||
def render_cpu(page, revision, system, process):
|
||||
return page.evaluate('''({revision,system,process}) => {
|
||||
window.rustProxyWideOverview.render({revision,live_system:{
|
||||
cpu_percent:system, process_cpu_percent:process
|
||||
}});
|
||||
const read = name => {
|
||||
const prefix = `overview-wide-wide_cpu-${name}`;
|
||||
return {
|
||||
path: document.getElementById(`${prefix}-line`).getAttribute('d'),
|
||||
label: document.getElementById(`${prefix}-value`).textContent,
|
||||
};
|
||||
};
|
||||
return {system:read('system'),process:read('process')};
|
||||
}''', {'revision': revision, 'system': system, 'process': process})
|
||||
|
||||
|
||||
def ys(path):
|
||||
return [int(v) for v in re.findall(r'[ML]\d+\s+(\d+)', path)]
|
||||
|
||||
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
|
||||
for viewport in (320, 375, 768, 1440, 1920):
|
||||
page = browser.new_page(viewport={'width': viewport, 'height': 950})
|
||||
errors = []
|
||||
page.on('pageerror', lambda error: errors.append(str(error)))
|
||||
page.set_content(HTML.replace('/assets/app___RUSTPROXY_ASSET_ID__.js', '')
|
||||
.replace('/assets/style___RUSTPROXY_ASSET_ID__.css', ''))
|
||||
page.add_style_tag(content=CSS)
|
||||
page.evaluate('''() => {
|
||||
document.documentElement.classList.remove('app-loading', 'app-startup-failed');
|
||||
document.getElementById('workspace').hidden = false;
|
||||
document.getElementById('panel-overview').hidden = false;
|
||||
const grid = document.getElementById('dashboard-overview-grid');
|
||||
grid.classList.add('is-aligned');
|
||||
for (const card of grid.querySelectorAll(':scope > [data-dashboard-widget]')) {
|
||||
card.hidden = !['wide_cpu', 'wide_protocol_rates', 'backend_traffic', 'connection_chart'].includes(card.dataset.dashboardWidget);
|
||||
}
|
||||
window.currentPanel = 'overview';
|
||||
window.$ = id => document.getElementById(id);
|
||||
window.fmt = (value, digits = 0) => Number(value).toFixed(digits);
|
||||
window.fmtBytes = value => `${value} B`;
|
||||
window.rustProxyOverviewSamples = {getLimit: () => 60, onChange: () => {}};
|
||||
}''')
|
||||
page.add_script_tag(content=SCALE_JS)
|
||||
page.add_script_tag(content=WIDE_JS)
|
||||
|
||||
for density in ('compact', 'normal', 'ultra'):
|
||||
page.evaluate('(density) => document.documentElement.dataset.dashboardDensity = density', density)
|
||||
result = inspect(page)
|
||||
assert result['protocolHeight'] >= 124, (viewport, density, result)
|
||||
assert abs(result['cpuHeight'] - 125) <= 1, (
|
||||
'CPU height must remain fixed at 125px', viewport, density, result)
|
||||
assert result['backendHeight'] > 0, (viewport, density, result)
|
||||
assert result['ratio'] == 'none' and result['cpuSeries'] == 2, result
|
||||
assert result['lineEffect'] == result['gridEffect'] == 'non-scaling-stroke', result
|
||||
assert result['gridPath'] == 'M0 47H600 M0 77H600 M0 107H600', result
|
||||
assert result['svgWidth'] > 100 and abs(result['svgWidth'] - result['drawnWidth']) < 2, result
|
||||
assert result['legendInCard'] and result['overflow'] <= 2, (viewport, density, result)
|
||||
|
||||
# Low measured CPU values stay near zero instead of filling the plot.
|
||||
first = render_cpu(page, 1, 0.5, 0.2)
|
||||
second = render_cpu(page, 1, 0.8, 0.6)
|
||||
assert first['system']['label'] == '0.5%' and second['process']['label'] == '0.6%'
|
||||
assert ys(second['system']['path']) == [136, 136], (viewport, second)
|
||||
assert ys(second['process']['path']) == [137, 136], (viewport, second)
|
||||
|
||||
# The Y axis is always 0-100% regardless of recorded history.
|
||||
third = render_cpu(page, 1, 3.4, 2.2)
|
||||
assert ys(third['system']['path'])[-1] == 133, (viewport, third)
|
||||
assert ys(third['process']['path'])[-1] == 134, (viewport, third)
|
||||
high = render_cpu(page, 1, 250, 123)
|
||||
assert high['system']['label'] == '250.0%' and high['process']['label'] == '123.0%'
|
||||
assert ys(high['system']['path'])[-1] == 17, (viewport, high)
|
||||
assert ys(high['process']['path'])[-1] == 17, (viewport, high)
|
||||
mid = render_cpu(page, 1, 50, 25)
|
||||
assert ys(mid['system']['path'])[-1] == 77, (viewport, mid)
|
||||
assert ys(mid['process']['path'])[-1] == 107, (viewport, mid)
|
||||
|
||||
# Revision changes reset history; missing readings do not plot a zero.
|
||||
idle = render_cpu(page, 2, 0, 0)
|
||||
assert ys(idle['system']['path']) == [137] and ys(idle['process']['path']) == [137]
|
||||
full = render_cpu(page, 2, 100, 100)
|
||||
assert ys(full['system']['path'])[-1] == 17 and ys(full['process']['path'])[-1] == 17
|
||||
missing = render_cpu(page, 3, None, None)
|
||||
assert missing['system']['path'] == '' and missing['process']['label'] == '-'
|
||||
|
||||
# Samples may change paths, but not the plot/card or neighboring card sizes.
|
||||
for density in ('compact', 'normal', 'ultra'):
|
||||
page.evaluate('(density) => document.documentElement.dataset.dashboardDensity = density', density)
|
||||
unchanged = inspect(page)
|
||||
assert abs(unchanged['cpuHeight'] - 125) <= 1, (viewport, density, unchanged)
|
||||
for value in (0.1, 99.9, 250, 0.0):
|
||||
before = inspect(page)
|
||||
render_cpu(page, 4, value, value)
|
||||
after = inspect(page)
|
||||
for key in ('cpuHeight', 'cpuCardHeight', 'connectionCardHeight'):
|
||||
assert abs(before[key] - after[key]) < 1, (viewport, density, value, key, before, after)
|
||||
assert not errors, (viewport, errors)
|
||||
print(f'PASS CPU fixed 0-100% Y-scale and 125px plot height at {viewport}px (all densities)')
|
||||
page.close()
|
||||
browser.close()
|
||||
@@ -39,7 +39,7 @@ const context = vm.createContext({
|
||||
t: key => lang[key] || key,
|
||||
console,
|
||||
});
|
||||
for (const script of ['app/rate-sampling.js', 'app/overview-extra-metrics.js', 'app/overview-wide-metrics.js']) {
|
||||
for (const script of ['app/rate-sampling.js', 'app/chart-scales.js', 'app/overview-extra-metrics.js', 'app/overview-wide-metrics.js']) {
|
||||
vm.runInContext(code(script), context, { filename: script });
|
||||
}
|
||||
const extra = window.rustProxyExtraOverview, wide = window.rustProxyWideOverview;
|
||||
|
||||
@@ -42,7 +42,7 @@ const context = vm.createContext({
|
||||
$: 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'])
|
||||
for (const file of ['app/rate-sampling.js', 'app/chart-scales.js', 'app/overview-wide-metrics.js', 'app/web-overview.js'])
|
||||
vm.runInContext(code(file), context, { filename: file });
|
||||
|
||||
const web = {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
#!/usr/bin/env python3
|
||||
"""0.33.6 plot size/scale checks in simulated Chromium, not a running RustProxy."""
|
||||
from pathlib import Path
|
||||
import re
|
||||
from playwright.sync_api import sync_playwright
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
HTML = (ROOT / 'src/admin/web/index.html').read_text(encoding='utf-8')
|
||||
CSS = (ROOT / 'src/admin/web/style.css').read_text(encoding='utf-8')
|
||||
SCALES = (ROOT / 'src/admin/web/app/chart-scales.js').read_text(encoding='utf-8')
|
||||
RATES = (ROOT / 'src/admin/web/app/rate-sampling.js').read_text(encoding='utf-8')
|
||||
WIDE = (ROOT / 'src/admin/web/app/overview-wide-metrics.js').read_text(encoding='utf-8')
|
||||
|
||||
WIDE_IDS = ('wide_protocol_rates', 'wide_cpu', 'wide_memory', 'wide_cache_activity',
|
||||
'wide_connections', 'wide_backend_health', 'wide_latency',
|
||||
'wide_transform_activity', 'wide_compression_activity',
|
||||
'www_requests', 'www_latency', 'www_failures', 'www_traffic', 'www_status_trend')
|
||||
|
||||
with sync_playwright() as api:
|
||||
browser = api.chromium.launch(headless=True, executable_path='/usr/bin/chromium', args=['--no-sandbox'])
|
||||
for width in (320, 375, 768, 1440, 1920):
|
||||
page = browser.new_page(viewport={'width': width, 'height': 920})
|
||||
errors = []
|
||||
page.on('pageerror', lambda error: errors.append(str(error)))
|
||||
page.set_content(HTML.replace('/assets/app___RUSTPROXY_ASSET_ID__.js', '')
|
||||
.replace('/assets/style___RUSTPROXY_ASSET_ID__.css', ''))
|
||||
page.add_style_tag(content=CSS)
|
||||
page.evaluate('''() => {
|
||||
document.documentElement.classList.remove('app-loading','app-startup-failed');
|
||||
document.getElementById('workspace').hidden = false;
|
||||
document.getElementById('panel-overview').hidden = false;
|
||||
document.getElementById('dashboard-overview-grid').classList.add('is-aligned');
|
||||
for (const card of document.querySelectorAll('#dashboard-overview-grid>[data-dashboard-widget]'))
|
||||
card.hidden = !['wide_protocol_rates','wide_cpu','wide_memory','wide_cache_activity',
|
||||
'wide_connections','wide_backend_health','wide_latency',
|
||||
'wide_transform_activity','wide_compression_activity',
|
||||
'www_requests','www_latency','www_failures','www_traffic','www_status_trend'].includes(card.dataset.dashboardWidget);
|
||||
window.currentPanel='overview';
|
||||
window.$ = id => document.getElementById(id);
|
||||
window.fmt = (n, digits=0) => Number(n).toFixed(digits);
|
||||
window.fmtBytes = n => `${n} B`;
|
||||
window.rustProxyOverviewSamples = {getLimit:()=>60,onChange:()=>{}};
|
||||
window.rustProxyRefresh = {current:()=>({overview_seconds:3})};
|
||||
window.__clock = 1000;
|
||||
Object.defineProperty(performance,'now',{configurable:true,value:()=>window.__clock});
|
||||
}''')
|
||||
for script in (RATES, SCALES, WIDE):
|
||||
page.add_script_tag(content=script)
|
||||
for density in ('normal','compact','ultra'):
|
||||
measured = page.evaluate('''density => {
|
||||
document.documentElement.dataset.dashboardDensity=density;
|
||||
const results={};
|
||||
for (const card of document.querySelectorAll('#dashboard-overview-grid > .overview-wide-chart')) {
|
||||
const svg=card.querySelector('svg.overview-wide-svg');
|
||||
if (!svg) continue;
|
||||
const r=svg.getBoundingClientRect();
|
||||
results[card.dataset.dashboardWidget]={height:r.height, width:r.width,
|
||||
grid:svg.querySelector('.chart-grid').getAttribute('d'),
|
||||
contentWidth:Math.hypot(svg.getScreenCTM().a,svg.getScreenCTM().b)*600};
|
||||
}
|
||||
document.getElementById('panel-transform').hidden=false;
|
||||
document.getElementById('panel-cache').hidden=false;
|
||||
for(const id of ['transform-live-chart','cache-live-chart'])document.getElementById(id).open=true;
|
||||
results.transformDetails=document.getElementById('transform-live-activity-chart').getBoundingClientRect().height;
|
||||
results.cacheDetails=document.getElementById('cache-live-activity-chart').getBoundingClientRect().height;
|
||||
return results;
|
||||
}''', density)
|
||||
assert abs(measured['transformDetails'] - 180) < 1, (width,density,measured)
|
||||
assert abs(measured['cacheDetails'] - 180) < 1, (width,density,measured)
|
||||
for name in WIDE_IDS:
|
||||
data=measured[name]
|
||||
assert abs(data['height']-125)<1, (width,density,name,data)
|
||||
assert data['width']>50 and abs(data['width']-data['contentWidth'])<2, (width,density,name,data)
|
||||
assert data['grid']=='M0 47H600 M0 77H600 M0 107H600', (width,density,name,data)
|
||||
result = page.evaluate('''() => {
|
||||
const c=window.rustProxyChartScale;
|
||||
const first=c.peak('probe:rate','rate',[[.2,.7]]);
|
||||
const count=c.peak('probe:count','count',[[0,1]]);
|
||||
const latency=c.peak('probe:ms','ms',[[.1,1.5]]);
|
||||
const memory=c.peak('probe:bytes','bytes',[[1000]]);
|
||||
const high=c.peak('probe:rate','rate',[[101]]);
|
||||
const held=c.peak('probe:rate','rate',[[.3]]);
|
||||
c.reset('probe:');
|
||||
const reset=c.peak('probe:rate','rate',[[.3]]);
|
||||
const percent=c.peak('probe:cpu','percent',[[.1,500]]);
|
||||
const blank=c.peak('probe:blank','rate',[[null,NaN,Infinity]]);
|
||||
const before={revision:10,live_system:{cpu_percent:0.7,process_cpu_percent:0.2},
|
||||
metrics:{compression_gzip:50,compression_br:100,compression_zstd:10,compression_deflate:5},
|
||||
transform_metrics:{applied:2,cache_hits:10,skipped:1,errors:0}};
|
||||
window.rustProxyWideOverview.render(before);
|
||||
window.__clock=4000;
|
||||
before.metrics.compression_gzip+=3;before.metrics.compression_br+=6;
|
||||
before.metrics.compression_zstd+=0;before.metrics.compression_deflate+=0;
|
||||
before.transform_metrics.applied+=6;before.transform_metrics.cache_hits+=3;
|
||||
window.rustProxyWideOverview.render(before);
|
||||
const get = (id,key) => document.getElementById(`overview-wide-${id}-${key}-line`).getAttribute('d');
|
||||
const compressionLow=get('wide_compression_activity','gzip');
|
||||
const transformLow=get('wide_transform_activity','applied');
|
||||
const cpu=get('wide_cpu','system');
|
||||
const lowLabel=document.getElementById('overview-wide-wide_compression_activity-gzip-value').textContent;
|
||||
window.__clock=7000;
|
||||
before.metrics.compression_gzip+=603; // now 201/s => ceiling expands to 500/s
|
||||
before.transform_metrics.applied+=603;
|
||||
window.rustProxyWideOverview.render(before);
|
||||
const highLabel=document.getElementById('overview-wide-wide_compression_activity-gzip-value').textContent;
|
||||
const compressionHigh=get('wide_compression_activity','gzip');
|
||||
window.__clock=10000;
|
||||
before.metrics.compression_gzip+=3;
|
||||
before.transform_metrics.applied+=3;
|
||||
window.rustProxyWideOverview.render(before);
|
||||
const afterSpike=get('wide_compression_activity','gzip');
|
||||
return {first,count,latency,memory,high,held,reset,percent,blank,
|
||||
lowLabel,highLabel,compressionLow,transformLow,cpu,compressionHigh,afterSpike};
|
||||
}''')
|
||||
assert result['first']==100 and result['count']==10 and result['latency']==100, (width,result)
|
||||
assert result['memory']>=64*1024*1024, (width,result)
|
||||
assert (result['high'],result['held'],result['reset'],result['percent'],result['blank']) == (200,200,100,100,100), (width,result)
|
||||
assert result['lowLabel']=='1.0/s' and result['highLabel']=='201.0/s', (width,result)
|
||||
def yvalues(line): return [int(n) for n in re.findall(r'[ML]\d+ (\d+)',line)]
|
||||
assert yvalues(result['compressionLow'])[-1]>=135, (width,result)
|
||||
assert yvalues(result['transformLow'])[-1]>=130, (width,result)
|
||||
assert yvalues(result['cpu'])==[136,136], (width,result)
|
||||
assert yvalues(result['compressionHigh'])[-1]<100, (width,result)
|
||||
assert yvalues(result['afterSpike'])[-1]>=135, (width,result)
|
||||
assert not errors, (width,errors)
|
||||
print('PASS all 14 overview-wide plot sizes/grids + full Cache/Transform charts + stable scales:', width)
|
||||
page.close()
|
||||
browser.close()
|
||||
@@ -93,6 +93,7 @@ assert.equal(again.window.rustProxyOverviewSamples.getLimit(), 300);
|
||||
// Wide Overview charts must respect the same limit and redraw immediately when
|
||||
// the limit shrinks, without recording an extra point or hitting the network.
|
||||
vm.runInContext(read('src/admin/web/app/rate-sampling.js'), context);
|
||||
vm.runInContext(read('src/admin/web/app/chart-scales.js'), context);
|
||||
vm.runInContext(read('src/admin/web/app/overview-wide-metrics.js'), context);
|
||||
select.value = '600'; select.fire('change');
|
||||
const status = {
|
||||
|
||||
Reference in new issue
Block a user