Обзор
Бенчмарки
perfscale поставляется с бенчмарк-скриптом на основе наборов тестов (suites), сравнивающим движки под идентичной нагрузкой:
./scripts/bench.sh # all suites
SUITES="throughput startup" ./scripts/bench.sh
Каждый сценарий обращается к одному и тому же экземпляру perfscale serve на
loopback, поэтому любая разница между строкой perfscale (*) и её «родным»
аналогом — это накладные расходы обёртки perfscale, а не самого инструмента.
| Сценарий | Что выполняется |
|---|---|
locust (native) | locust, вызванный напрямую — базовый уровень |
k6 (native) | k6, вызванный напрямую — базовый уровень |
jmeter (native) | jmeter -n -t, вызванный напрямую — базовый уровень (только наборы overhead/startup) |
perfscale (k6) | тот же скрипт k6 через perfscale run --k6 |
perfscale (locust) | тот же locustfile через perfscale run --locust |
perfscale (jmeter) | тот же .jmx-план через perfscale run --jmeter (только наборы overhead/startup) |
perfscale (yaml) | собственный шаговый движок perfscale (без внешних бинарников) |
perfscale (yaml quiet) | шаговый движок с --quiet — логирование каждого запроса отключено; разница с perfscale (yaml) — это стоимость логирования |
Наборы тестов (suites)
| Набор | На какой вопрос отвечает |
|---|---|
overhead | Добавляет ли обёртка время выполнения при фиксированной длительности? (hyperfine, статистическое усреднение) |
throughput | Сколько запросов и с какой задержкой реально обеспечивает каждый сценарий? Плюс CPU, CPU на запрос, пиковый RSS и операции ввода-вывода из того же инструментированного прогона |
startup | Сколько стоит запуск обёртки? (прогоны по 1 с, где старт не «тонет» в длительности теста) |
scaling | Как растут RPS / p95 / RSS с ростом числа VU? (стандартная развёртка: 10, 50, 200) |
saturation | Приблизительный максимум RPS на движок при большом числе VU (по умолчанию 256) |
yaml | Сколько стоит каждая возможность нативного движка? (базовый GET против --quiet против +check против POST-тела против многошаговой подстановки) |
ws | Пропускная способность WebSocket-сообщений и RTT: эхо-запросы по одному соединению к эндпоинту /ws сервера serve (perfscale (yaml), k6 (native), perfscale (k6) — у locust нет встроенной поддержки WebSocket, он пропускается) |
boundary | Где движок ломается? Линейный рост 0→BOUNDARY_MAX_VUS за BOUNDARY_DURATION с отчётом об уровне VU / времени / RPS, при которых накопленная доля ошибок впервые достигает BOUNDARY_ERR_PCT процентов (по умолчанию 1%) |
tls | «Налог» TLS: та же нагрузка против perfscale serve --tls (самоподписанный HTTPS, проверка отключена) |
JMeter участвует только в наборах hyperfine (overhead/startup): его
консольная сводка в non-GUI режиме не содержит перцентилей, поэтому он не
попадает в таблицу throughput. Парсинг его .jtl-файлов результатов для
полных метрик пропускной способности — задача на будущее.
Наборы выбираются через SUITES="...". Сценарии, чей движок (k6/locust/
jmeter) отсутствует в PATH, пропускаются, а не считаются упавшими;
overhead/startup требуют
hyperfine, остальные — нет.
Набор boundary определяет пересечение границы по-разному для каждого
движка: k6 сам прерывается через порог abortOnFail (границей считается
строка прогресса на пике VU), locust читается из строк Aggregated в
--csv-full-history, jmeter — из накопленных дельт summary + (запускается с
-Jsummariser.interval=2), а нативный движок — из периодических строк
[stats] — поэтому строка нативной границы выполняется без --quiet и
включает стоимость логирования каждого запроса. Строки для обёрнутых
locust/jmeter опущены: обёртка — это просто проксирование, а граница —
свойство движка.
Микробенчмарки горячих путей нативного движка (парсинг YAML, подстановка
${{ ... }}, запись метрик / перцентильная сводка, захват вывода процессов
через RingBuf, матчеры готовности waitUntil) находятся в
crates/perfscale-core/benches/ и запускаются командой cargo bench -p perfscale-core.
GPU-инференс (только локально)
Отдельный набор для бенчмарков LLM-инференса на GPU (Ollama /
vLLM через std/llm@v1, с метриками прогона gpu: — TTFT, токены/сек,
утилизация GPU/VRAM) находится в bench/gpu/. Он
намеренно не входит в CI-workflow выше: на хостовых раннерах нет GPU, поэтому
набор выполняется на локальной машине с картой NVIDIA.
Методология
- Цель: экземпляр
perfscale serveна loopback (GET /health; наборwsиспользует его WebSocket-эхо-эндпоинтGET /ws) — без сетевого шума, одна и та же цель для каждого сценария в рамках прогона. Наборtlsдобавляет второй экземплярserve --tls. - Сравнение по времени выполнения (
overhead,startup): hyperfine повторяет каждый сценарий (--runs, с--warmup) и сообщает среднее, min/max и стандартное отклонение. - Пропускная способность/задержка/ресурсы: один инструментированный
прогон на сценарий под
/usr/bin/time(-vна Linux,-lна macOS), где запросы, RPS и перцентили задержки парсятся из собственной сводки движка (текстовая сводка k6, статистика locust--csv, единые строки сводки perfscale). Один прогон: ориентировочно, без статистической строгости. - Осторожно с фиксированной длительностью: каждый сценарий выполняется одинаковое время, поэтому все времена выполнения набора overhead ≈ сама длительность. Относительные числа около 1.00 доказывают, что обёртка ничего не добавляет; реальные различия между движками — в числах пропускной способности.
- Общий CPU: генератор нагрузки и
perfscale serveделят одну машину. Потолки насыщения включают стоимость цели; если два движка выходят на плато при похожих RPS — подозревайте цель/CPU, а не движок.
Локальный запуск
cargo build --release
./scripts/bench.sh # all suites, 10 VUs, 15s, 5 runs
VUS=50 DURATION=30s ./scripts/bench.sh
SUITES="yaml" YAML_DURATION=5s ./scripts/bench.sh # just the native-engine suite
OUTPUT=report.md RESULTS=results.json ./scripts/bench.sh
| Переменная | По умолчанию | Описание |
|---|---|---|
SUITES | все | Список наборов через пробел (см. таблицу выше) |
VUS | 10 | Виртуальных пользователей на сценарий |
DURATION | 15s | Длительность прогона для overhead/throughput |
WARMUP | 1 | Разогревочные прогоны hyperfine (отбрасываются, не показываются) |
RUNS | 5 | Измеряемых прогонов hyperfine на сценарий |
STARTUP_DURATION / STARTUP_RUNS | 1s / 5 | Длительность прогона / число замеров набора startup |
SCALING_VUS / SCALING_DURATION | 10 50 200 / 10s | Точки развёртки VU / длительность на точку |
SAT_VUS / SAT_DURATION | 256 / 15s | VU насыщения / длительность прогона |
YAML_DURATION / TLS_DURATION | 10s / 10s | Длительность на сценарий в этих наборах |
WS_DURATION / WS_ROUNDS | 10s / 10 | Длительность прогона набора ws / эхо-циклов на соединение |
BOUNDARY_DURATION / BOUNDARY_MAX_VUS / BOUNDARY_ERR_PCT | 30s / 2000 / 1 | длительность разгона / цель разгона / порог доли ошибок в процентах для набора boundary |
PORT / TLS_PORT | 18999 / 18998 | Порты для одноразовых целей serve |
OUTPUT | bench-report.md | Путь к Markdown-отчёту |
RESULTS | bench-results.json | Машиночитаемые результаты (вход для отслеживания регрессий) |
PERFSCALE_BIN | target/release/perfscale | Тестируемый бинарник; собирается при отсутствии |
Запуск в CI (канонический)
Workflow bench выполняется на
ubuntu-latest — фиксированный класс раннера, что устраняет разброс между
локальными машинами:
- вручную: GitHub → Actions →
bench→ Run workflow (параметры:vus,duration,runs,suites) - еженедельно: по расписанию, понедельник 04:00 UTC, как проверка дрейфа производительности
Сводка задачи содержит таблицу окружения (ОС/CPU/потоки/RAM/swap) и таблицу
ПО (версии perfscale/k6/locust/jmeter/java/hyperfine) перед результатами.
Артефакт bench-report (хранится 90 дней) содержит markdown-отчёт и
bench-results.json.
Отслеживание регрессий
Каждый CI-прогон скачивает bench-results.json предыдущего успешного прогона
и добавляет в сводку таблицу дельт (scripts/bench_compare.py): RPS,
задержка, RSS, накладные расходы на запуск и средние criterion, с пометкой
изменений свыше ±15%. Раннеры — общее оборудование, поэтому регрессии
предупреждают, а не роняют задачу — воспринимайте пометку как «посмотри
сюда», подтверждая повторным прогоном.
Как читать числа
overhead: относительное время выполнения около 1.00 = обёртка бесплатна на этой длительности. Реальные различия движков — вthroughput.throughput: сравнивайте RPS и перцентили;CPU per reqнормализует стоимость CPU по фактически выполненной работе (движок, потребляющий в 2 раза больше CPU при в 10 раз большем RPS, эффективнее, а не меньше).startup:vs native— собственная стоимость обёртки;vs idealвключает также старт самого движка.- Никогда не сравнивайте между машинами или CI-прогонами — только внутри одного отчёта (таблица дельт сравнивает между прогонами на одном классе раннеров, а это наиболее близкое к корректному сравнение).
Как читать IO ops (in / out)
N in / M out — это счётчик файловых операций из прогона под
/usr/bin/time:
in— операции чтения: блоки, прочитанные с файловой системы (загрузка скриптов/конфигов, подкачка бинарников).0 in— норма для тёплого прогона: всё уже в страничном кэше. Холодный первый прогон показывает ненулевоеin(например, k6 подкачивает свой бинарник).out— операции записи: блоки, записанные на файловую систему (временные скрипты, статистика locust--csv, логи).
Единицы IO ops различаются между ОС (GNU time считает блоки ФС на Linux; BSD time считает блочные операции на macOS), поэтому одно и то же число не означает одинаковый объём байт в разных системах — сравнивайте только внутри одного отчёта.