Производительность25 сентября 2026 г. · 8 мин чтения

WASM-библиотеки: генерируйте любое значение в любом payload — теперь и на TypeScript

perfscale v0.21/v0.22 превращают слой токенов ${...} в настоящую точку расширения: библиотеки генераторов значений в сандбоксе wasmtime, офлайн-распространение через install/lockfile — и, начиная с сегодняшнего дня, TypeScript SDK, компилирующий ваши генераторы в WASM-компоненты.

Автор: Команда Perfscale

WASM-библиотеки: генерируйте любое значение в любом payload — теперь и на TypeScript

Каждый нагрузочный тест рано или поздно упирается в значение, которое инструмент генерировать отказывается. Настоящий 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 сегодня обязательна.

Сравнение

k6JMeterperfscale
Свои генераторы значенийxk6-расширение, пересборка бинаряplugin jar или инлайн Groovy/JSR223один .wasm + одна строка YAML
Языки авторингатолько GoJava/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-порт встроенной библиотеки, служит шаблоном для авторов. Если опубликуете свою библиотеку — нам очень интересно.