Обзор

Бенчмарки

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всеСписок наборов через пробел (см. таблицу выше)
VUS10Виртуальных пользователей на сценарий
DURATION15sДлительность прогона для overhead/throughput
WARMUP1Разогревочные прогоны hyperfine (отбрасываются, не показываются)
RUNS5Измеряемых прогонов hyperfine на сценарий
STARTUP_DURATION / STARTUP_RUNS1s / 5Длительность прогона / число замеров набора startup
SCALING_VUS / SCALING_DURATION10 50 200 / 10sТочки развёртки VU / длительность на точку
SAT_VUS / SAT_DURATION256 / 15sVU насыщения / длительность прогона
YAML_DURATION / TLS_DURATION10s / 10sДлительность на сценарий в этих наборах
WS_DURATION / WS_ROUNDS10s / 10Длительность прогона набора ws / эхо-циклов на соединение
BOUNDARY_DURATION / BOUNDARY_MAX_VUS / BOUNDARY_ERR_PCT30s / 2000 / 1длительность разгона / цель разгона / порог доли ошибок в процентах для набора boundary
PORT / TLS_PORT18999 / 18998Порты для одноразовых целей serve
OUTPUTbench-report.mdПуть к Markdown-отчёту
RESULTSbench-results.jsonМашиночитаемые результаты (вход для отслеживания регрессий)
PERFSCALE_BINtarget/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), поэтому одно и то же число не означает одинаковый объём байт в разных системах — сравнивайте только внутри одного отчёта.