Files
loadtest/site/templates/index.html
T

421 lines
18 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<!-- LoadTest - страница проверки загрузки файлов. -->
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LoadTest - проверка загрузки файлов</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="wrap">
<h1>LoadTest - проверка загрузки файлов</h1>
<p class="version">Сборка: v{{ version }} <span id="healthInfo"></span></p>
<p class="hint">
Выберите ОДИН файл (в т.ч. большой) и нажмите «Загрузить».
Файл читается в память Flask (на диск не пишется). Результат: файл дошёл или нет + статистика.
</p>
<!-- Панель выбора файла и кнопка загрузки -->
<div class="panel">
<label for="file">Файл:</label>
<input type="file" id="file">
<button id="btn" onclick="startUpload()">Загрузить</button>
</div>
<!-- Индикатор прогресса: полоса, % и таймер -->
<div class="progress-wrap" id="progressWrap" style="display:none">
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-info">
<span id="progressPct">0%</span>
<span id="progressTimer">0с</span>
<span id="progressSpeed"></span>
</div>
</div>
<!-- Итоговый статус: УСПЕХ / ОШИБКА -->
<div id="status"></div>
<!-- Максимум статистики (клиент + сервер) -->
<pre id="result"></pre>
<!-- ============================================================ -->
<!-- СЕКЦИЯ СООБЩЕНИЙ (проверка очередей/ИоТ) -->
<!-- ============================================================ -->
<hr style="margin:24px 0;border:none;border-top:2px dashed #ccc;">
<h2>Сообщение (очередь / ИоТ)</h2>
<p class="hint">
Задайте размер сообщения (в байтах). Содержимое - случайное.
Проверяется, ДОХОДИТ ли тело сообщения нужного размера до бэкенда (без multipart, как в очереди).
</p>
<div class="panel">
<label for="msgSize">Размер (байт):</label>
<input type="number" id="msgSize" value="1024" min="1" step="1" style="width:180px;">
<button id="btnMsg" onclick="startMessage()">Отправить сообщение</button>
</div>
<!-- Прогресс отправки сообщения -->
<div class="progress-wrap" id="msgProgressWrap" style="display:none">
<div class="progress-bar">
<div class="progress-fill" id="msgProgressFill"></div>
</div>
<div class="progress-info">
<span id="msgProgressPct">0%</span>
<span id="msgProgressTimer">0с</span>
<span id="msgProgressSpeed"></span>
</div>
</div>
<!-- Итоговый статус сообщения -->
<div id="msgStatus"></div>
<!-- Статистика сообщения -->
<pre id="msgResult"></pre>
</div>
<script>
// ---------- Проверка приёма СООБЩЕНИЯ (очередь/ИоТ) ----------
// Тело - сырое (raw), без multipart, со случайным содержимым заданного размера.
function startMessage() {
var sizeEl = document.getElementById('msgSize');
var size = parseInt(sizeEl.value, 10);
var status = document.getElementById('msgStatus');
var result = document.getElementById('msgResult');
var wrap = document.getElementById('msgProgressWrap');
result.textContent = '';
status.textContent = '';
if (!size || size < 1) {
status.textContent = 'ОШИБКА: укажите размер';
status.className = 'status error';
return;
}
// ---- Генерируем случайное содержимое нужного размера ----
var startMs = Date.now();
var startLocal = new Date().toLocaleTimeString();
var randomStr = randomBytes(size); // строка случайных символов длиной size
// ---- Прогресс ----
wrap.style.display = 'block';
var fill = document.getElementById('msgProgressFill');
var pctEl = document.getElementById('msgProgressPct');
var timerEl = document.getElementById('msgProgressTimer');
var speedEl = document.getElementById('msgProgressSpeed');
fill.style.width = '0%';
pctEl.textContent = '0%';
timerEl.textContent = '0с';
speedEl.textContent = '';
var timer = setInterval(function () {
var sec = Math.floor((Date.now() - startMs) / 1000);
timerEl.textContent = sec + 'с';
}, 1000);
var xhr = new XMLHttpRequest();
xhr.open('POST', '/messages');
xhr.timeout = 600000;
// Шлём как текстовое тело (как очередь обычно шлёт payload).
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.upload.onprogress = function (e) {
if (!e.lengthComputable) { pctEl.textContent = '?%'; return; }
var pct = Math.round(e.loaded / e.total * 100);
fill.style.width = pct + '%';
pctEl.textContent = pct + '%';
var sec = (Date.now() - startMs) / 1000;
if (sec > 0) speedEl.textContent = ((e.loaded / (1024 * 1024)) / sec).toFixed(2) + ' МБ/с';
};
xhr.onload = function () {
clearInterval(timer);
var sec = ((Date.now() - startMs) / 1000).toFixed(2);
var httpStatus = xhr.status;
var ok = xhr.status >= 200 && xhr.status < 300;
var data = null;
try { data = JSON.parse(xhr.responseText); } catch (e) { data = null; }
if (ok && data && data.ok) {
status.textContent = '✅ СООБЩЕНИЕ ПРИНЯТО (' + size + ' байт)';
status.className = 'status ok';
renderMsgStats(size, startLocal, sec, httpStatus, data);
} else {
status.textContent = '❌ СООБЩЕНИЕ НЕ ПРИНЯТО (HTTP ' + httpStatus + ')';
status.className = 'status error';
var errBody = data && data.error ? 'ошибка: ' + data.error : xhr.responseText;
result.textContent =
'HTTP статус: ' + httpStatus + '\n' +
'Ответ сервера: ' + (errBody || '(пусто)') + '\n' +
msgClientStats(size, startLocal, sec);
}
};
xhr.onerror = function () {
clearInterval(timer);
var sec = ((Date.now() - startMs) / 1000).toFixed(2);
status.textContent = '❌ СЕТЕВАЯ ОШИБКА - ответа нет';
status.className = 'status error';
result.textContent = msgClientStats(size, startLocal, sec) +
'\n(соединение прервано, прокси/платформа не ответила)';
};
xhr.ontimeout = function () {
clearInterval(timer);
var sec = ((Date.now() - startMs) / 1000).toFixed(2);
status.textContent = '❌ ТАЙМАУТ (10 мин) - сообщение не принято';
status.className = 'status error';
result.textContent = msgClientStats(size, startLocal, sec);
};
xhr.send(randomStr);
}
// ---- Генерация случайной строки заданной длины ----
function randomBytes(n) {
var chars = 'ABCDEF0123456789abcdefghijklmnopqrstuvwxyz';
var out = '';
// Генерируем частями (по 8192) - для очень больших n без зависания строки.
var chunk = 8192;
while (out.length < n) {
var need = Math.min(chunk, n - out.length);
var s = '';
for (var i = 0; i < need; i++) s += chars[Math.floor(Math.random() * chars.length)];
out += s;
}
return out;
}
// ---- Клиентская статистика сообщения ----
function msgClientStats(size, startLocal, sec) {
var avg = sec > 0 ? (size / (1024 * 1024) / sec).toFixed(2) : '0';
return '-- КЛИЕНТ (сообщение) --\n' +
'Размер: ' + size + ' байт (' + (size / 1024).toFixed(3) + ' КБ)\n' +
'Начало: ' + startLocal + '\n' +
'Длительность: ' + sec + ' с\n' +
'Средняя скорость: ' + avg + ' МБ/с\n';
}
// ---- Полная статистика при успехе ----
function renderMsgStats(size, startLocal, sec, httpStatus, data) {
var m = data.message;
var s = data.server;
var meta = data.meta;
var avg = sec > 0 ? (size / (1024 * 1024) / sec).toFixed(2) : '0';
var out = '';
out += '-- КЛИЕНТ (сообщение) --\n' +
'Размер: ' + size + ' байт (' + (size / 1024).toFixed(3) + ' КБ)\n' +
'Начало: ' + startLocal + '\n' +
'Длительность: ' + sec + ' с\n' +
'Средняя скорость: ' + avg + ' МБ/с\n' +
'HTTP статус: ' + httpStatus + '\n\n';
out += '-- СЕРВЕР (что реально дошло) --\n' +
'Версия сборки: ' + (data.version || '?') + '\n' +
'Принято байт: ' + m.size_bytes + ' (' + m.size_kb + ' КБ)\n' +
'Чтение в память: ' + s.read_ms + ' мс\n' +
'Обработка всего: ' + s.total_ms + ' мс\n' +
'Скорость (сервер): ' + s.speed_mb_s + ' МБ/с\n' +
'MD5 принятых байт: ' + data.md5 + '\n' +
'Префикс тела: ' + m.prefix + '\n\n';
out += '-- ЗАПРОС --\n' +
'Время сервера: ' + meta.server_start + '\n' +
'Remote addr: ' + meta.remote_addr + '\n' +
'Content-Length (заголовок): ' + meta.content_length_header + '\n' +
'Content-Type (заголовок): ' + meta.content_type_header + '\n' +
'User-Agent: ' + meta.user_agent;
result.textContent = out;
}
</script>
<script>
// ---------- Загрузка одного файла через XMLHttpRequest ----------
// XHR даёт реальный прогресс отправки (fetch - не даёт), поэтому берём XHR.
function startUpload() {
var input = document.getElementById('file');
var file = input.files[0];
// Сброс старых данных.
var result = document.getElementById('result');
var status = document.getElementById('status');
var wrap = document.getElementById('progressWrap');
result.textContent = '';
status.textContent = '';
if (!file) {
status.textContent = 'ОШИБКА: выберите файл';
status.className = 'status error';
return;
}
// ---- Статистика клиента ----
var startMs = Date.now(); // время начала загрузки (клиент)
var fileSize = file.size; // размер файла (байт)
var fileSizeMB = fileSize / (1024 * 1024);
var startLocal = new Date().toLocaleTimeString();
// ---- Прогресс: полоса + % ----
wrap.style.display = 'block';
var fill = document.getElementById('progressFill');
var pctEl = document.getElementById('progressPct');
var timerEl = document.getElementById('progressTimer');
var speedEl = document.getElementById('progressSpeed');
fill.style.width = '0%';
pctEl.textContent = '0%';
timerEl.textContent = '0с';
speedEl.textContent = '';
// Таймер (второстепенно): обновляем прошедшие секунды каждую 1с.
var timer = setInterval(function () {
var sec = Math.floor((Date.now() - startMs) / 1000);
timerEl.textContent = sec + 'с';
}, 1000);
// ---- Формируем multipart-запрос ----
var fd = new FormData();
fd.append('file', file, file.name); // поле должно называться 'file' (см. app.py)
var xhr = new XMLHttpRequest();
xhr.open('POST', '/upload');
// Таймаут 10 минут - большие файлы могут ехать долго.
xhr.timeout = 600000;
// Реальный прогресс отправки тела запроса (loaded - отправлено байт).
xhr.upload.onprogress = function (e) {
if (!e.lengthComputable) {
pctEl.textContent = '?%';
return;
}
var pct = Math.round(e.loaded / e.total * 100);
fill.style.width = pct + '%';
pctEl.textContent = pct + '%';
// Мгновенная средняя скорость клиента.
var sec = (Date.now() - startMs) / 1000;
if (sec > 0) {
var mbps = (e.loaded / (1024 * 1024)) / sec;
speedEl.textContent = mbps.toFixed(2) + ' МБ/с';
}
};
// ---- Ответ сервера получен ----
xhr.onload = function () {
clearInterval(timer);
var sec = ((Date.now() - startMs) / 1000).toFixed(2);
var httpStatus = xhr.status;
var ok = xhr.status >= 200 && xhr.status < 300;
// Сервер может ответить и НЕ-JSON (413, HTML ошибки прокси) - показываем текст.
var data = null;
try { data = JSON.parse(xhr.responseText); } catch (e) { data = null; }
if (ok && data && data.ok) {
// УСПЕХ - файл дошёл до бэкенда.
status.textContent = '✅ ФАЙЛ ЗАГРУЖЕН';
status.className = 'status ok';
renderStats(file, fileSize, fileSizeMB, startLocal, sec, httpStatus, data);
} else {
// ОШИБКА - показываем статус, текст сервера и клиентскую статистику.
status.textContent = '❌ ФАЙЛ НЕ ЗАГРУЖЕН (HTTP ' + httpStatus + ')';
status.className = 'status error';
var errBody = data && data.error ? 'ошибка: ' + data.error : xhr.responseText;
result.textContent =
'HTTP статус: ' + httpStatus + '\n' +
'Ответ сервера: ' + (errBody || '(пусто)') + '\n' +
clientStats(file, fileSize, fileSizeMB, startLocal, sec);
}
};
// ---- Сеть: соединение оборвано, нет ответа ----
xhr.onerror = function () {
clearInterval(timer);
var sec = ((Date.now() - startMs) / 1000).toFixed(2);
status.textContent = '❌ СЕТЕВАЯ ОШИБКА - ответа нет';
status.className = 'status error';
result.textContent = clientStats(file, fileSize, fileSizeMB, startLocal, sec) +
'\n(соединение прервано, прокси/платформа не ответила)';
};
// ---- Таймаут ----
xhr.ontimeout = function () {
clearInterval(timer);
var sec = ((Date.now() - startMs) / 1000).toFixed(2);
status.textContent = '❌ ТАЙМАУТ (10 мин) - файл не загружен';
status.className = 'status error';
result.textContent = clientStats(file, fileSize, fileSizeMB, startLocal, sec);
};
xhr.send(fd);
}
// ---- Клиентская статистика (общая для всех исходов) ----
function clientStats(file, size, sizeMB, startLocal, sec) {
var avg = sec > 0 ? (size / (1024 * 1024) / sec).toFixed(2) : '0';
return '-- КЛИЕНТ --\n' +
'Файл: ' + file.name + '\n' +
'Размер: ' + size + ' байт (' + sizeMB.toFixed(3) + ' МБ)\n' +
'Начало: ' + startLocal + '\n' +
'Длительность: ' + sec + ' с\n' +
'Средняя скорость: ' + avg + ' МБ/с\n';
}
// ---- Полная статистика при успехе: клиент + сервер ----
function renderStats(file, size, sizeMB, startLocal, sec, httpStatus, data) {
var f = data.file;
var s = data.server;
var m = data.meta;
var avg = sec > 0 ? (size / (1024 * 1024) / sec).toFixed(2) : '0';
var out = '';
out += '-- КЛИЕНТ --\n' +
'Файл: ' + file.name + '\n' +
'Размер: ' + size + ' байт (' + sizeMB.toFixed(3) + ' МБ)\n' +
'Начало: ' + startLocal + '\n' +
'Длительность: ' + sec + ' с\n' +
'Средняя скорость (клиент): ' + avg + ' МБ/с\n' +
'HTTP статус: ' + httpStatus + '\n\n';
out += '-- СЕРВЕР (что реально дошло) --\n' +
'Имя файла: ' + f.name + '\n' +
'Версия сборки: ' + (data.version || '?') + '\n' +
'MIME: ' + f.mime + '\n' +
'Принято байт: ' + f.size_bytes + ' (' + f.size_mb + ' МБ)\n' +
'Чтение в память: ' + s.read_ms + ' мс\n' +
'Обработка всего: ' + s.total_ms + ' мс\n' +
'Скорость (сервер): ' + s.speed_mb_s + ' МБ/с\n' +
'MD5 принятых байт: ' + data.md5 + '\n\n';
out += '-- ЗАПРОС --\n' +
'Время сервера: ' + m.server_start + '\n' +
'Remote addr: ' + m.remote_addr + '\n' +
'Content-Length (заголовок): ' + m.content_length_header + '\n' +
'Content-Type (заголовок): ' + m.content_type_header + '\n' +
'User-Agent: ' + m.user_agent;
result.textContent = out;
}
// ---- Показать фактическую версию сборки с /health ----
function showRuntimeVersion() {
var el = document.getElementById('healthInfo');
var xhr = new XMLHttpRequest();
xhr.open('GET', '/health');
xhr.timeout = 5000;
xhr.onload = function () {
if (el) el.textContent = '| /health: ' + (xhr.responseText || '');
};
xhr.onerror = function () { if (el) el.textContent = '| /health: недоступен'; };
xhr.send();
}
showRuntimeVersion();
</script>
</body>
</html>