Ядро

Библиотеки — пользовательские генераторы значений (WASM)

Каждый payload в perfscale может содержать токены-генераторы ${...} — ${seq}, ${uuid}, ${rand(1,100)} — которые движок подставляет при отправке сообщения. Библиотеки — это точка расширения: библиотека добавляет собственные функции, которые можно вызывать из любого payload как ${alias.fn(...)} с одинаковым синтаксисом во всех протоколах (HTTP, WebSocket, gRPC, GraphQL, сырой TCP/UDP, LLM-промпты, параметры БД).

Библиотека — это либо встроенный модуль, поставляемый с движком (@std/random), либо WASM-компонент, написанный вами, — эта страница рассматривает оба варианта с акцентом на создание собственных. Полное описание конфигурации находится в YAML-справочнике; обоснование дизайна — в RFC 005.

Объявление и использование

libraries:
  - use: '@std/random@v1'        # built-in, no files needed
    capabilities: []             # required key — [] means "no grants"

steps:
  - use: std/db-query@v1
    with:
      query: INSERT INTO orders (id, code, who) VALUES (?, ?, ?)
      params:
        - "${random.ulid()}"
        - "${random.pattern("ORD-####-^^")}"
        - "${random.email()}"
  • use: — ссылка на встроенную библиотеку @ns/name@vN (кавычки обязательны), локальный путь к .wasm (относительно файла, в котором она объявлена), HTTPS-URL (требует sha256:) или ссылка вида git+<repo>@<ref>#<path>.
  • as: — префикс токена. По умолчанию — собственное имя библиотеки.
  • capabilities: — явные разрешения для песочницы, см. ниже. Обязателен в каждой записи — пустой список (capabilities: []) означает «без разрешений». Отсутствие ключа — ошибка валидации. Миграция со старых документов: добавьте capabilities: [] в каждую запись без разрешений.
  • with: — JSON-конфигурация, передаваемая в init() библиотеки; может использовать ${{ env.X }}, чтобы секреты оставались замаскированными в логах.

Удалённые источники (https:, git+) никогда не скачиваются во время выполнения: perfscale install скачивает каждый из них один раз, проверяет хеш, сохраняет в content-addressed кеш и записывает perfscale.lock рядом с объявляющим файлом — закоммитьте lock-файл. Затем run и lint разрешают всё полностью офлайн.

Кратко о семантике токенов:

  • Встроенные токены (${seq}, ${rand}, …) сопоставляются первыми и не изменяются; alias не может их перекрыть.
  • Неизвестный alias → токен остаётся как есть (обратная совместимость; perfscale lint помечает его). Известный alias + неизвестная функция → шаг завершается с ошибкой — опечатка не должна незаметно попасть в payload.
  • Необязательный завершающий аргумент key любой функции мемоизирует результат в пределах одного сообщения: два ${random.uuid4(order)} в одном сообщении дают один и тот же id; следующее сообщение генерирует новый.
  • seed: 42 в конфигурации делает прогон воспроизводимым — каждый экземпляр библиотеки выводит свой seed как hash(seed, vu_id, conn_seq).
  • Ошибка вызова (ошибка гостя, trap, таймаут) завершает шаг с ошибкой и зафиксированной причиной — нагрузочный тест никогда не отправит неверные данные, отчитавшись об успехе.

Встроенная библиотека @std/random@v1

Поставляется с движком — без WASM, без установки, без capabilities. Каждая функция принимает необязательный завершающий ключ мемоизации key:

ФункцияВозвращает
uuid4([key]) / uuid7([key])Случайный v4 / упорядоченный по времени v7 UUID
ulid([key])26-символьный ULID в Crockford-base32
nanoid([len], [key])URL-безопасный id (длина по умолчанию 21)
int(a, b, [key])Случайное целое в [a, b]
float(a, b, [dp], [key])Случайное число с плавающей точкой, dp знаков после запятой
pick(a|b|c, [key])Случайный выбор среди вариантов, разделённых |
weighted(a:10|b:90, [key])Взвешенный случайный выбор
seq(name)Именованный монотонный счётчик, начинается с 1
pattern("ORD-####-????")Заполнение шаблона: #→цифра, ?→a-z, ^→A-Z, *→буква или цифра
first_name() / last_name() / name()Случайные имена
username() / email() / company()Случайный username / email / название компании
lorem([words])Слова lorem-ipsum (по умолчанию 5)
phone()Случайный номер телефона
date(a, b) / timestamp(a, b) / datetime(a, b)Случайные даты и метки времени в диапазоне

Capabilities и песочница

Пользовательские библиотеки выполняются в песочнице wasmtime по fail-closed модели capability: WASI-импорты компонента сверяются с разрешениями capabilities: в YAML, а компонент, импортирующий больше, чем разрешено, — это жёсткая ошибка загрузки. Единственное исключение — самообъявленная чистая библиотека: когда info() компонента сообщает "pure": true, его импорты wasi:filesystem и wasi:clocks/wall-clock считаются шумом тулчейна (компоненты TS/JS, собранные jco, всегда их содержат) и разрешение не требуется. Песочница при этом продолжает действовать — без разрешения fs гость получает ноль preopen'ов, поэтому любой реальный доступ к файлам завершится ошибкой во время выполнения.

CapabilityЧто предоставляет хостГраницы
fspreopen'ы wasi:filesystemтолько пути внутри fs_root, только чтение
clockwasi:clockswall time также приходит через контекст вызова

Любое непустое разрешение требует allow_library_capabilities: true в конфигурации — тот же fail-closed паттерн, что и allow_file_actions. Сам ключ capabilities: обязателен в каждой записи libraries: (включая встроенные): пишите capabilities: [], когда библиотеке не нужны разрешения, — отсутствие ключа является ошибкой валидации при загрузке, lint и run. Сырые сокеты никогда не предоставляются. wasi:random тоже не предоставляется: библиотеки получают всю случайность из PRNG с seed'ом, который предоставляет их SDK, — именно это делает прогоны с seed: воспроизводимыми. Fuel на каждый вызов и таймаут по wall time ограничивают то, что сбежавший компонент может натворить на горячем пути, а время внутри вызова библиотеки учитывается в длительности охватывающего шага — стоимость библиотеки видна в метриках и никогда не скрыта.

Запись библиотеки также может ограничивать и маскировать свою поверхность:

libraries:
  - use: ./libs/fixer-ids.wasm
    capabilities: []       # no sandbox grants
    allow: [uuid4, ulid]   # only these functions callable
    log:
      secret: true         # mask every result of this library in the run log

Написание собственной библиотеки

Одна форма, три языка — каждый SDK скрывает WIT-инфраструктуру (perfscale:library@0.2.0, WASI Preview 2) и предоставляет PRNG с seed'ом (бит-идентичный во всех SDK и во встроенном генераторе движка), memo() для повторного использования в пределах сообщения, вспомогательные функции для аргументов и тестовое окружение без рантайма.

TypeScript / JavaScript — @perfscale/library-sdk

SDK и инструменты сборки находятся в репозитории sdk-libraries; установка из npm:

$ npm install @perfscale/library-sdk
import { args, defineLibrary } from "@perfscale/library-sdk";

export default defineLibrary({
  name: "mylib",
  functions: {
    token: {
      description: "Deterministic per-instance token; memo key reuses it within one message",
      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);
      },
    },
  },
});

Сборка в WASM-компонент (ComponentizeJS из jco встраивает JS-движок):

$ npx perfscale-library-build mylib.ts -o mylib.wasm

Затем укажите .wasm в libraries: и вызывайте ${mylib.token()} в payload'ах. Ctx из SDK содержит messageSeq, iterationSeq, vuId, seed, timeMs, а также ctx.memo() и ctx.rng(); testCall запускает юнит-тесты под node:test без участия WASM.

Замечание о песочнице: компоненты jco всегда импортируют wasi:filesystem и wasi:clocks/wall-clock. TS-библиотеке, объявляющей "pure": true в метаданных info(), не нужно разрешение capabilities: для этих импортов — движок связывает интерфейсы, но не предоставляет preopen'ов, поэтому реальный доступ к файлам всё равно завершится ошибкой. Библиотеки, которые действительно читают файлы, по-прежнему объявляют capabilities: [fs] (одно разрешение fs покрывает оба интерфейса).

Rust — perfscale-library-sdk

Полный SDK в основном репозитории: crates/perfscale-library-sdk, собирается в wasm32-wasip2 без дополнительного тулчейна. Репозиторий library-random — WASM-порт @std/random — одновременно служит эталонной реализацией и шаблоном для авторов.

Go — документированный рецепт (экспериментальный)

Пошаговое руководство по TinyGo + wasm32-wasip2 + wit-bindgen-go в go/README.md репозитория sdk-libraries. Готового SDK-пакета пока нет — WIT-контракт тот же, поэтому сгенерированные биндинги работают с любым релизом perfscale, поддерживающим perfscale:library@0.2.0.

Начало с шаблона

library-template — минимальная готовая к сборке библиотека (greet + мемоизированный токен) с подключённым SDK — склонируйте, переименуйте и добавляйте функции.

Доступность и линтинг

Библиотеки — часть YAML теста/конфигурации, поэтому одно и то же определение работает и из CLI, и на ваших машинах. Встроенной @std/random вообще не нужны файлы; пользовательские WASM-компоненты должны присутствовать на генераторе нагрузки (относительные пути разрешаются относительно объявляющего файла, удалённые ссылки берутся из кеша install). perfscale lint загружает объявленные библиотеки и проверяет каждый токен ${alias.fn(...)} на соответствие функциям, которые они реально экспортируют, — опечатки отлавливаются до прогона.