Ядро
Библиотеки — пользовательские генераторы значений (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 | Что предоставляет хост | Границы |
|---|---|---|
fs | preopen'ы wasi:filesystem | только пути внутри fs_root, только чтение |
clock | wasi:clocks | wall 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(...)} на
соответствие функциям, которые они реально экспортируют, — опечатки
отлавливаются до прогона.