Каждый нагрузочный тест рано или поздно упирается в значение, которое инструмент генерировать отказывается. Настоящий UUID v7, а не 32 hex-символа. ULID. FIX ClOrdID, который встречается в трёх полях одного сообщения и должен быть одинаковым во всех трёх. Faker-имена и email. Подписанный токен из вашего собственного сервиса.
В k6 ответ — «найдите xk6-расширение или напишите своё и пересоберите бинарь». В JMeter — «найдите plugin jar или спрячьте Groovy в __groovy()». В обоих случаях нужный вам генератор — это сначала проект сборки, и только потом ассет теста.
perfscale v0.21.0 и v0.22.0 приносят библиотеки (RFC 005): WASM-компоненты, добавляющие функции в слой токенов ${...}, вызываемые из любого payload в любом протоколе:
libraries:
- use: '@std/random@v1' # встроенная, поставляется с движком
- use: ./libs/fixer-ids.wasm # ваш собственный компонент
steps:
- use: std/db-query@v1
with:
query: INSERT INTO orders (id, code, who) VALUES (?, ?, ?)
params:
- "${random.ulid()}"
- "${fix.clordid(new)}"
- "${random.email()}"
Один синтаксис — ${alias.fn(...)} — для HTTP, WebSocket, gRPC, GraphQL, сырого TCP/UDP, промптов LLM и параметров БД. Без пересборки движка, без охоты за плагинами.
Встроенная библиотека: @std/random@v1
Ещё до всякого WASM движок поставляет нативную встроенную библиотеку на типовые случаи: uuid4/uuid7, ulid, nanoid, int(a,b), float(a,b,dp), pick(a|b|c), weighted(a:10|b:90), именованные счётчики seq, заполнение шаблонов pattern("ORD-####-????"), faker-имена/email/username/компании, lorem, телефонные номера и диапазоны дат/таймстампов. Вся настройка — это - use: '@std/random@v1': без файлов, без install, без capabilities.
Две семантики делают её больше, чем игрушкой:
- Мемо-ключи. Опциональный последний аргумент любой функции фиксирует результат в пределах одного сообщения: два
${random.uuid4(order)}дают один id, а${random.uuid4()}всегда свежий. Проблема ClOrdID решена как языковая фича, а не как костыль. - Детерминизм прогона.
seed: 42в конфиге делает воспроизводимым каждый генератор в прогоне — seed экземпляра выводится какhash(seed, vu_id, conn_seq). Падающий нагрузочный тест, который можно переиграть байт-в-байт, — это инструмент отладки, а не роскошь.
И одно fail-closed правило, которое мы отстаиваем: неизвестный алиас остаётся как есть (обратная совместимость), но известный алиас с неизвестной функцией роняет шаг. Опечатка не должна молча уезжать в payload и рапортовать зелёным.
Свои библиотеки: WASM в настоящем сандбоксе
Когда встроенной не хватает, вы пишете компонент. Движок загружает WASI Preview 2 компоненты в сандбокс wasmtime с fail-closed моделью capabilities:
- WASI-импорты компонента сверяются с грантом
capabilities:в вашем YAML — хост подключает ровно их пересечение.fs— это read-only preopens в пределахfs_root;clock— монотонные часы. Сырые сокеты не предоставляются никогда, а любой грант требуетallow_library_capabilities: true— тот же паттерн, что уallow_file_actions. wasi:randomне предоставляется вообще. Библиотеки черпают энтропию из seeded PRNG своего SDK — именно это держит честными прогоны сseed:.- Fuel на вызов и таймаут по wall-time ограничивают взбесившийся компонент, а время внутри вызова библиотеки учитывается в длительности шага: медленная библиотека видна в ваших метриках латентности ровно там, где она и есть. Упавший вызов — trap, таймаут, ошибка гостя — роняет шаг с записанной причиной.
Распространение следует дисциплине «явный install; рантайм офлайн». Удалённые ссылки (https://… с обязательным sha256:-пином или git+<repo>@<ref>#<path>) скачиваются один раз командой perfscale install, проверяются по дайджесту, складываются в контентно-адресуемый кэш и записываются в perfscale.lock, который вы коммитите. Дальше run и lint резолвят всё полностью офлайн — отсутствующая запись в локе это жёсткая ошибка, а не молчаливая скачка.
Пишем на TypeScript
Сегодня мы также выпускаем @perfscale/library-sdk v0.1.0 — TypeScript/JavaScript SDK, пополняющий Rust SDK, вышедший с v0.21.0:
import { args, defineLibrary } from "@perfscale/library-sdk";
export default defineLibrary({
name: "mylib",
functions: {
token: {
description: "Детерминированный токен на экземпляр; мемо-ключ переиспользует его в одном сообщении",
call(argv, ctx) {
const mint = () => `tok-${ctx.rng().nextU64().toString(16)}`;
const key = args.optionalString(argv, 0);
return key === undefined ? mint() : ctx.memo(key, mint);
},
},
},
});
Сборка в компонент — через CLI из пакета: ComponentizeJS встраивает JS-движок, и именно так TypeScript становится WASM:
$ npx perfscale-library-build mylib.ts -o mylib.wasm
SDK даёт контекст вызова (messageSeq, iterationSeq, vuId, seed, timeMs), ctx.memo() для мемо-семантики и ctx.rng() — seeded xorshift64 PRNG, бит-в-бит совпадающий с Rust SDK и встроенным генератором движка, так что seeded-прогоны воспроизводятся независимо от языка библиотеки. testCall гоняет вашу библиотеку в обычном node:test без всякого WASM-рантайма. Авторам на Go доступен задокументированный рецепт на TinyGo в том же репозитории.
Одна честная оговорка для TS-авторов: jco-компоненты всегда импортируют wasi:filesystem, поэтому TypeScript-библиотеке нужен грант fs, даже если она логически чистая. Грант — это read-only preopen fs_root прогона, а сборка отрезает wasi:random, wasi:http и таймеры — но строка гранта в YAML сегодня обязательна.
Сравнение
| k6 | JMeter | perfscale | |
|---|---|---|---|
| Свои генераторы значений | xk6-расширение, пересборка бинаря | plugin jar или инлайн Groovy/JSR223 | один .wasm + одна строка YAML |
| Языки авторинга | только Go | Java/Kotlin (или скрипты в payload) | Rust, TypeScript/JS, Go |
| Сандбоксинг | нет — код расширения это сам движок | нет — плагины в том же процессе | сандбокс wasmtime, fail-closed capabilities, без сырых сокетов |
| Дистрибуция | пересборка под набор расширений | jar в classpath каждого генератора | perfscale install + закоммиченный lockfile, офлайн-прогоны |
| Воспроизводимые генерируемые значения | нет | нет | seed: в конфиге, бит-идентичный PRNG во всех SDK |
| Единый синтаксис для всех протоколов | по расширениям | по функциям | ${alias.fn(...)} в каждом payload |
Честные издержки
wasmtime не бесплатен: это минуты времени сборки и заметно больший бинарь для каждого пользователя, включая тех, кто никогда не загрузит библиотеку, — мы сознательно приняли это вместо раздвоения матрицы сборок. Сандбокс — постоянное обязательство по безопасности, а не законченная фича. И ничто не мешает автору библиотеки делать что-то медленное на каждый вызов на горячем пути; ответ движка — fuel, таймауты и per-library p95 метрики, которые делают боттleneck видимым, а не предотвращают его конструкцией.
Сетевой egress намеренно отсутствует пока: он появится как host-mediated HTTP по allowlist, а не как сырые сокеты, потому что allowlist enforceable только если соединение проходит через хоста.
Попробовать
$ perfscale run -f test.yaml -c config.yaml
Полная поверхность конфигурации — в YAML-справочнике, концептуальная страница — в нашей документации, обоснование дизайна — в RFC 005. SDK для авторов живут в perfscale-library-sdk (Rust) и sdk-libraries (TS/JS v0.1.0, рецепт для Go) — а library-random, WASM-порт встроенной библиотеки, служит шаблоном для авторов. Если опубликуете свою библиотеку — нам очень интересно.
