feat: speed test through the browser, client - panel - browser (0.0.6)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b51363bf72
commit
87921a1381
9 files changed
+344
-33
No files matched your search
@@ -19,6 +19,7 @@
|
||||
.speed { display:flex; gap:28px; flex-wrap:wrap; margin-top:16px; }
|
||||
.speed b { display:block; font-size:20px; font-variant-numeric:tabular-nums; }
|
||||
.speed span { color:var(--muted); font-size:13px; }
|
||||
label.check { display:inline-flex; align-items:center; gap:8px; margin:0; color:var(--text); font-size:14px; cursor:pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -57,15 +58,19 @@
|
||||
<div class="card">
|
||||
<h2>Скорость соединения</h2>
|
||||
<p class="muted">Клиент замеряет свой канал до хоста панели - тот же, по которому идут терминал, файлы и http-проброс.
|
||||
С галочкой «До браузера» данные идут дальше, через панель до этого браузера и обратно: так виден весь путь клиент - панель - браузер.
|
||||
Замер занимает около 7 секунд и на это время нагружает канал.</p>
|
||||
<div class="row">
|
||||
<button id="speed-btn" type="button">Замерить скорость</button>
|
||||
<label class="check" title="Данные пойдут не только между клиентом и хостом панели, а через панель до этого браузера и обратно">
|
||||
<input type="checkbox" id="speed-relay"> До браузера
|
||||
</label>
|
||||
<span class="muted" id="speed-note"></span>
|
||||
</div>
|
||||
<div class="speed" id="speed" hidden>
|
||||
<div><b id="sp-ping"></b><span>задержка</span></div>
|
||||
<div><b id="sp-down"></b><span>хост → клиент</span></div>
|
||||
<div><b id="sp-up"></b><span>клиент → хост</span></div>
|
||||
<div><b id="sp-down"></b><span id="sp-down-lbl"></span></div>
|
||||
<div><b id="sp-up"></b><span id="sp-up-lbl"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -134,17 +139,93 @@
|
||||
load();
|
||||
|
||||
var btn = $('speed-btn'), note = $('speed-note');
|
||||
function json(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }
|
||||
function post(path) { return fetch(base + path, {method: 'POST', credentials: 'same-origin'}).then(json); }
|
||||
|
||||
// The browser's round trip to the panel: the first request may pay for a
|
||||
// new connection, so keep the best.
|
||||
function panelPing() {
|
||||
var best = Infinity, left = 4;
|
||||
function one() {
|
||||
var t = performance.now();
|
||||
return fetch('/api/panel/ping', {credentials: 'same-origin', cache: 'no-store'})
|
||||
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.arrayBuffer(); })
|
||||
.then(function () {
|
||||
best = Math.min(best, performance.now() - t);
|
||||
return --left > 0 ? one() : best;
|
||||
});
|
||||
}
|
||||
return one();
|
||||
}
|
||||
// Keeps the client's download fed until stopped or the panel says the
|
||||
// client has read enough; the pieces grow while they go through quickly.
|
||||
function feed(stop) {
|
||||
var block = new Uint8Array(65536), n = 4;
|
||||
crypto.getRandomValues(block);
|
||||
function one() {
|
||||
var parts = [], t = performance.now();
|
||||
for (var i = 0; i < n; i++) parts.push(block);
|
||||
return fetch(base + '/speed/down', {method: 'POST', body: new Blob(parts), credentials: 'same-origin', signal: stop.signal})
|
||||
.then(function (r) {
|
||||
if (!r.ok) return;
|
||||
if (performance.now() - t < 500 && n < 256) n *= 2;
|
||||
return one();
|
||||
});
|
||||
}
|
||||
one().catch(function () {});
|
||||
}
|
||||
// Times what the client sends, from its first byte to the end of the stream.
|
||||
function drain(onFirst) {
|
||||
return fetch(base + '/speed/up', {credentials: 'same-origin', cache: 'no-store'}).then(function (r) {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
var rd = r.body.getReader(), n = 0, start = 0;
|
||||
function pump() {
|
||||
return rd.read().then(function (c) {
|
||||
if (c.done) return start && n ? n * 8000 / Math.max(1, performance.now() - start) : 0;
|
||||
if (!start) { start = performance.now(); onFirst(); }
|
||||
n += c.value.length;
|
||||
return pump();
|
||||
});
|
||||
}
|
||||
return pump();
|
||||
});
|
||||
}
|
||||
// The test through the browser: the client's download is what this page
|
||||
// uploads, and its upload is timed here, where it arrives.
|
||||
function relayTest() {
|
||||
return panelPing().then(function (ping) {
|
||||
return post('/speed/relay').then(function (res) {
|
||||
if (!res.ok) return res;
|
||||
var stop = new AbortController();
|
||||
var up = drain(function () { stop.abort(); });
|
||||
up.catch(function () {});
|
||||
feed(stop);
|
||||
return post('/speed?relay=1').then(function (res) {
|
||||
stop.abort();
|
||||
if (!res.ok) return res;
|
||||
return up.then(function (bps) {
|
||||
res.data.ping_ms += ping;
|
||||
res.data.up_bps = bps;
|
||||
return res;
|
||||
});
|
||||
}, function (e) { stop.abort(); throw e; });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
btn.onclick = function () {
|
||||
var relay = $('speed-relay').checked;
|
||||
btn.disabled = true;
|
||||
note.textContent = 'Идёт замер…';
|
||||
fetch(base + '/speed', {method: 'POST', credentials: 'same-origin'})
|
||||
.then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
||||
(relay ? relayTest() : post('/speed'))
|
||||
.then(function (res) {
|
||||
if (!res.ok) { note.textContent = res.error || 'ошибка'; return; }
|
||||
var d = res.data;
|
||||
$('sp-ping').textContent = d.ping_ms.toFixed(d.ping_ms < 10 ? 1 : 0) + ' мс';
|
||||
$('sp-down').textContent = rate(d.down_bps);
|
||||
$('sp-up').textContent = rate(d.up_bps);
|
||||
$('sp-down-lbl').textContent = relay ? 'браузер → клиент' : 'хост → клиент';
|
||||
$('sp-up-lbl').textContent = relay ? 'клиент → браузер' : 'клиент → хост';
|
||||
$('speed').hidden = false;
|
||||
note.textContent = '';
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user