@fedify/cli 2.3.0-dev.1219 → 2.3.0-dev.1258
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bench/action.js +203 -0
- package/dist/bench/actor/documents.js +39 -0
- package/dist/bench/actor/fleet.js +39 -0
- package/dist/bench/actor/keys.js +35 -0
- package/dist/bench/command.js +42 -0
- package/dist/bench/discovery/discover.js +67 -0
- package/dist/bench/discovery/probe.js +50 -0
- package/dist/bench/load/arrival.js +27 -0
- package/dist/bench/load/clock.js +15 -0
- package/dist/bench/load/generator.js +112 -0
- package/dist/bench/metrics/aggregate.js +64 -0
- package/dist/bench/metrics/histogram.js +141 -0
- package/dist/bench/metrics/stats-client.js +154 -0
- package/dist/bench/mod.js +4 -0
- package/dist/bench/render/format.js +46 -0
- package/dist/bench/render/index.js +20 -0
- package/dist/bench/render/json.js +12 -0
- package/dist/bench/render/markdown.js +62 -0
- package/dist/bench/render/text.js +74 -0
- package/dist/bench/result/build.js +129 -0
- package/dist/bench/result/expect/assert.js +74 -0
- package/dist/bench/result/expect/evaluate.js +128 -0
- package/dist/bench/result/expect/metrics.js +34 -0
- package/dist/bench/result/schema.js +15 -0
- package/dist/bench/safety/gate.js +54 -0
- package/dist/bench/safety/tiers.js +41 -0
- package/dist/bench/scenario/coerce.js +24 -0
- package/dist/bench/scenario/errors.js +36 -0
- package/dist/bench/scenario/load.js +69 -0
- package/dist/bench/scenario/normalize.js +126 -0
- package/dist/bench/scenario/schema.js +358 -0
- package/dist/bench/scenario/units.js +56 -0
- package/dist/bench/scenario/validate.js +29 -0
- package/dist/bench/scenarios/inbox.js +155 -0
- package/dist/bench/scenarios/registry.js +21 -0
- package/dist/bench/scenarios/runner.js +76 -0
- package/dist/bench/scenarios/webfinger.js +44 -0
- package/dist/bench/server/synthetic.js +118 -0
- package/dist/bench/signing/activity-id.js +18 -0
- package/dist/bench/signing/pipeline.js +134 -0
- package/dist/bench/signing/signer.js +39 -0
- package/dist/bench/template/generate.js +90 -0
- package/dist/bench/template/helpers.js +19 -0
- package/dist/bench/template/template.js +132 -0
- package/dist/cache.js +1 -1
- package/dist/config.js +14 -2
- package/dist/deno.js +1 -1
- package/dist/generate-vocab/action.js +3 -3
- package/dist/generate-vocab/command.js +1 -1
- package/dist/imagerenderer.js +1 -1
- package/dist/inbox.js +1 -1
- package/dist/lookup.js +34 -34
- package/dist/mod.js +3 -0
- package/dist/nodeinfo.js +6 -6
- package/dist/options.js +1 -1
- package/dist/runner.js +9 -8
- package/dist/tempserver.js +1 -1
- package/dist/tunnel.js +2 -2
- package/dist/utils.js +3 -2
- package/dist/webfinger/action.js +2 -2
- package/package.json +12 -10
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import "@js-temporal/polyfill";
|
|
2
|
+
import { asList } from "./coerce.js";
|
|
3
|
+
import { parseDuration, parseRate } from "./units.js";
|
|
4
|
+
//#region src/bench/scenario/normalize.ts
|
|
5
|
+
/**
|
|
6
|
+
* Normalizes a validated scenario suite into a fully resolved form the engine
|
|
7
|
+
* can execute: defaults applied, top-level scalar-or-list fields (`recipient`,
|
|
8
|
+
* `collection`, `fault`) coerced to arrays, durations and rates parsed to
|
|
9
|
+
* numbers, and the load model determined. Nested specs (`activity`, `source`)
|
|
10
|
+
* are passed through and coerced where they are consumed.
|
|
11
|
+
*
|
|
12
|
+
* It also enforces the cross-field rules that the JSON Schema cannot express,
|
|
13
|
+
* notably that the buffered signing modes require the target's signature time
|
|
14
|
+
* window to be off.
|
|
15
|
+
* @since 2.3.0
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
const DEFAULT_DURATION_MS = 6e4;
|
|
19
|
+
const DEFAULT_WARMUP_MS = 0;
|
|
20
|
+
const DEFAULT_RATE_PER_SEC = 50;
|
|
21
|
+
const DEFAULT_SIGNING = "pipeline";
|
|
22
|
+
const DEFAULT_RUNS = 1;
|
|
23
|
+
/** An error raised when a suite cannot be normalized. */
|
|
24
|
+
var SuiteNormalizeError = class extends Error {};
|
|
25
|
+
/**
|
|
26
|
+
* Normalizes a validated suite into resolved form.
|
|
27
|
+
* @param suite The validated suite.
|
|
28
|
+
* @param options Normalization options, such as a target override.
|
|
29
|
+
* @returns The resolved suite.
|
|
30
|
+
* @throws {SuiteNormalizeError} If the target is missing or a cross-field rule
|
|
31
|
+
* is violated.
|
|
32
|
+
*/
|
|
33
|
+
function normalizeSuite(suite, options = {}) {
|
|
34
|
+
const targetString = options.target ?? suite.target;
|
|
35
|
+
if (targetString == null || targetString.trim() === "") throw new SuiteNormalizeError("No target URL: set `target` in the suite or pass --target.");
|
|
36
|
+
let target;
|
|
37
|
+
try {
|
|
38
|
+
target = new URL(targetString);
|
|
39
|
+
} catch {
|
|
40
|
+
throw new SuiteNormalizeError(`Invalid target URL: ${targetString}.`);
|
|
41
|
+
}
|
|
42
|
+
if (target.protocol !== "http:" && target.protocol !== "https:" || target.hostname === "" || target.username !== "" || target.password !== "") throw new SuiteNormalizeError(`Invalid target URL ${JSON.stringify(targetString)}: a benchmark target must be an http: or https: URL with a host and no embedded credentials (for example http://localhost:3000).`);
|
|
43
|
+
return {
|
|
44
|
+
target,
|
|
45
|
+
actors: suite.actors ?? [],
|
|
46
|
+
scenarios: suite.scenarios.map((scenario) => resolveScenario(scenario, suite))
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function resolveScenario(scenario, suite) {
|
|
50
|
+
const defaults = suite.defaults ?? {};
|
|
51
|
+
const load = resolveLoad(defaults.load, scenario.load);
|
|
52
|
+
const signing = scenario.signing ?? defaults.signing ?? DEFAULT_SIGNING;
|
|
53
|
+
const signatureTimeWindow = scenario.signatureTimeWindow ?? defaults.signatureTimeWindow ?? false;
|
|
54
|
+
if (signing !== "jit" && signatureTimeWindow) throw new SuiteNormalizeError(`Scenario "${scenario.name}": ${signing} signing pre-signs requests, which requires the target's signature time window to be off; use signing: jit for a time-windowed target.`);
|
|
55
|
+
if (signing === "presign" && load.kind === "closed") throw new SuiteNormalizeError(`Scenario "${scenario.name}": presign signing needs a fixed request count, which a closed-loop (concurrency) load does not have; use an open-loop rate, or signing: pipeline or jit.`);
|
|
56
|
+
const durationMs = resolveDuration(scenario.duration ?? defaults.duration, DEFAULT_DURATION_MS);
|
|
57
|
+
const warmupMs = resolveDuration(scenario.warmup ?? defaults.warmup, DEFAULT_WARMUP_MS);
|
|
58
|
+
if (warmupMs >= durationMs) throw new SuiteNormalizeError(`Scenario "${scenario.name}": warmup (${warmupMs}ms) must be shorter than duration (${durationMs}ms); otherwise no requests are measured.`);
|
|
59
|
+
const runs = scenario.runs ?? defaults.runs ?? DEFAULT_RUNS;
|
|
60
|
+
if (runs > 1) throw new SuiteNormalizeError(`Scenario "${scenario.name}": multiple runs (runs > 1) are not yet implemented in fedify bench; set runs to 1.`);
|
|
61
|
+
return {
|
|
62
|
+
name: scenario.name,
|
|
63
|
+
type: scenario.type,
|
|
64
|
+
load,
|
|
65
|
+
durationMs,
|
|
66
|
+
warmupMs,
|
|
67
|
+
signing,
|
|
68
|
+
signatureTimeWindow,
|
|
69
|
+
runs,
|
|
70
|
+
recipients: asList(scenario.recipient),
|
|
71
|
+
inbox: scenario.inbox,
|
|
72
|
+
activity: scenario.activity,
|
|
73
|
+
authenticated: scenario.authenticated ?? false,
|
|
74
|
+
collections: asList(scenario.collection),
|
|
75
|
+
source: scenario.source,
|
|
76
|
+
sender: scenario.sender,
|
|
77
|
+
followers: scenario.followers,
|
|
78
|
+
queueDrainTimeoutMs: scenario.queueDrainTimeout == null ? void 0 : parseDuration(scenario.queueDrainTimeout),
|
|
79
|
+
faults: asList(scenario.fault),
|
|
80
|
+
expect: scenario.expect ?? {},
|
|
81
|
+
raw: scenario
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Resolves the load model from suite defaults and a scenario override. The
|
|
86
|
+
* scenario's choice of `rate`/`concurrency` wins outright (it selects the
|
|
87
|
+
* model), while compatible fields such as `arrival` and `maxInFlight` are
|
|
88
|
+
* inherited from the defaults when the scenario does not set them.
|
|
89
|
+
*/
|
|
90
|
+
function resolveLoad(defaults, scenario) {
|
|
91
|
+
const arrival = scenario?.arrival ?? defaults?.arrival ?? "constant";
|
|
92
|
+
const maxInFlight = scenario?.maxInFlight ?? defaults?.maxInFlight;
|
|
93
|
+
if (scenario?.concurrency != null) return {
|
|
94
|
+
kind: "closed",
|
|
95
|
+
concurrency: scenario.concurrency,
|
|
96
|
+
maxInFlight
|
|
97
|
+
};
|
|
98
|
+
if (scenario?.rate != null) return {
|
|
99
|
+
kind: "open",
|
|
100
|
+
ratePerSec: parseRate(scenario.rate),
|
|
101
|
+
arrival,
|
|
102
|
+
maxInFlight
|
|
103
|
+
};
|
|
104
|
+
if (defaults?.concurrency != null) return {
|
|
105
|
+
kind: "closed",
|
|
106
|
+
concurrency: defaults.concurrency,
|
|
107
|
+
maxInFlight
|
|
108
|
+
};
|
|
109
|
+
if (defaults?.rate != null) return {
|
|
110
|
+
kind: "open",
|
|
111
|
+
ratePerSec: parseRate(defaults.rate),
|
|
112
|
+
arrival,
|
|
113
|
+
maxInFlight
|
|
114
|
+
};
|
|
115
|
+
return {
|
|
116
|
+
kind: "open",
|
|
117
|
+
ratePerSec: DEFAULT_RATE_PER_SEC,
|
|
118
|
+
arrival,
|
|
119
|
+
maxInFlight
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function resolveDuration(value, fallback) {
|
|
123
|
+
return value == null ? fallback : parseDuration(value);
|
|
124
|
+
}
|
|
125
|
+
//#endregion
|
|
126
|
+
export { normalizeSuite };
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import "@js-temporal/polyfill";
|
|
2
|
+
//#region src/bench/scenario/schema.ts
|
|
3
|
+
/**
|
|
4
|
+
* The embedded JSON Schema (draft 2020-12) for benchmark scenario suite files.
|
|
5
|
+
*
|
|
6
|
+
* This object is the runtime copy used by the validator; it is published,
|
|
7
|
+
* byte-for-byte, as *schema/bench/scenario-v1.json* and a drift guard keeps the
|
|
8
|
+
* two in sync. The matching TypeScript types live in {@link ./types.ts}.
|
|
9
|
+
*
|
|
10
|
+
* The schema expresses every scenario type discussed for `fedify bench`
|
|
11
|
+
* (`inbox`, `webfinger`, `actor`, `object`, `fanout`, `collection`, `failure`,
|
|
12
|
+
* `mixed`), even though only `inbox` and `webfinger` have runners in this
|
|
13
|
+
* version. Three cross-field rules are enforced here rather than in code:
|
|
14
|
+
*
|
|
15
|
+
* - exactly one HTTP request signature scheme per actor group
|
|
16
|
+
* (`contains` + `minContains`/`maxContains`);
|
|
17
|
+
* - `rate` XOR `concurrency` in a load block (`oneOf`);
|
|
18
|
+
* - the allowed `expect` metrics per scenario type (`if`/`then` +
|
|
19
|
+
* `propertyNames`).
|
|
20
|
+
* @since 2.3.0
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
/** The hosted URL that serves the scenario schema. */
|
|
24
|
+
const SCENARIO_SCHEMA_ID = "https://json-schema.fedify.dev/bench/scenario-v1.json";
|
|
25
|
+
const READ_METRICS = [
|
|
26
|
+
"successRate",
|
|
27
|
+
"throughputPerSec",
|
|
28
|
+
"errors.total",
|
|
29
|
+
"errors.4xx",
|
|
30
|
+
"errors.5xx",
|
|
31
|
+
"latency.p50",
|
|
32
|
+
"latency.p95",
|
|
33
|
+
"latency.p99",
|
|
34
|
+
"latency.mean",
|
|
35
|
+
"latency.max"
|
|
36
|
+
];
|
|
37
|
+
const INBOX_METRICS = [
|
|
38
|
+
...READ_METRICS,
|
|
39
|
+
"signatureVerification.p50",
|
|
40
|
+
"signatureVerification.p95",
|
|
41
|
+
"signatureVerification.p99"
|
|
42
|
+
];
|
|
43
|
+
const FANOUT_METRICS = [
|
|
44
|
+
"successRate",
|
|
45
|
+
"deliveryThroughput",
|
|
46
|
+
"errors.total",
|
|
47
|
+
"errors.4xx",
|
|
48
|
+
"errors.5xx",
|
|
49
|
+
"queueDrain.p50",
|
|
50
|
+
"queueDrain.p95",
|
|
51
|
+
"queueDrain.p99"
|
|
52
|
+
];
|
|
53
|
+
const MIXED_METRICS = [...new Set([...INBOX_METRICS, ...FANOUT_METRICS])];
|
|
54
|
+
/** The benchmark scenario suite JSON Schema (draft 2020-12). */
|
|
55
|
+
const scenarioSchemaV1 = {
|
|
56
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
57
|
+
$id: SCENARIO_SCHEMA_ID,
|
|
58
|
+
title: "Fedify benchmark scenario suite",
|
|
59
|
+
type: "object",
|
|
60
|
+
required: ["version", "scenarios"],
|
|
61
|
+
additionalProperties: false,
|
|
62
|
+
properties: {
|
|
63
|
+
$schema: {
|
|
64
|
+
type: "string",
|
|
65
|
+
description: "An optional editor hint pointing at this schema."
|
|
66
|
+
},
|
|
67
|
+
version: { const: 1 },
|
|
68
|
+
target: {
|
|
69
|
+
type: "string",
|
|
70
|
+
format: "uri",
|
|
71
|
+
description: "The target base URL; may be overridden by --target."
|
|
72
|
+
},
|
|
73
|
+
defaults: { $ref: "#/$defs/defaults" },
|
|
74
|
+
actors: {
|
|
75
|
+
type: "array",
|
|
76
|
+
items: { $ref: "#/$defs/actorGroup" }
|
|
77
|
+
},
|
|
78
|
+
scenarios: {
|
|
79
|
+
type: "array",
|
|
80
|
+
minItems: 1,
|
|
81
|
+
items: { $ref: "#/$defs/scenario" }
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
$defs: {
|
|
85
|
+
duration: {
|
|
86
|
+
type: "string",
|
|
87
|
+
pattern: "^\\d+(\\.\\d+)?(ms|s|m|h)$",
|
|
88
|
+
description: "A duration such as 500ms, 30s, 2m, or 1h."
|
|
89
|
+
},
|
|
90
|
+
rate: {
|
|
91
|
+
description: "An open-loop arrival rate such as 200/s, or a number.",
|
|
92
|
+
oneOf: [{
|
|
93
|
+
type: "number",
|
|
94
|
+
exclusiveMinimum: 0
|
|
95
|
+
}, {
|
|
96
|
+
type: "string",
|
|
97
|
+
pattern: "^\\d+(\\.\\d+)?\\s*/\\s*(s|m|h)$"
|
|
98
|
+
}]
|
|
99
|
+
},
|
|
100
|
+
size: {
|
|
101
|
+
description: "A byte size such as 2KB or a plain number of bytes.",
|
|
102
|
+
oneOf: [{
|
|
103
|
+
type: "number",
|
|
104
|
+
minimum: 0
|
|
105
|
+
}, {
|
|
106
|
+
type: "string",
|
|
107
|
+
pattern: "^\\s*\\d+(\\.\\d+)?\\s*([Bb]|[Kk][Bb]|[Kk][Ii][Bb]|[Mm][Bb]|[Mm][Ii][Bb]|[Gg][Bb]|[Gg][Ii][Bb])?\\s*$"
|
|
108
|
+
}]
|
|
109
|
+
},
|
|
110
|
+
signatureStandard: { enum: [
|
|
111
|
+
"draft-cavage-http-signatures-12",
|
|
112
|
+
"rfc9421",
|
|
113
|
+
"ld-signatures",
|
|
114
|
+
"fep8b32"
|
|
115
|
+
] },
|
|
116
|
+
signingMode: { enum: [
|
|
117
|
+
"jit",
|
|
118
|
+
"pipeline",
|
|
119
|
+
"presign"
|
|
120
|
+
] },
|
|
121
|
+
arrival: { enum: ["constant", "poisson"] },
|
|
122
|
+
scalarOrListString: { oneOf: [{ type: "string" }, {
|
|
123
|
+
type: "array",
|
|
124
|
+
items: { type: "string" },
|
|
125
|
+
minItems: 1
|
|
126
|
+
}] },
|
|
127
|
+
load: {
|
|
128
|
+
type: "object",
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
properties: {
|
|
131
|
+
rate: { $ref: "#/$defs/rate" },
|
|
132
|
+
concurrency: {
|
|
133
|
+
type: "integer",
|
|
134
|
+
minimum: 1
|
|
135
|
+
},
|
|
136
|
+
arrival: { $ref: "#/$defs/arrival" },
|
|
137
|
+
maxInFlight: {
|
|
138
|
+
type: "integer",
|
|
139
|
+
minimum: 1
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
not: { required: ["rate", "concurrency"] }
|
|
143
|
+
},
|
|
144
|
+
defaults: {
|
|
145
|
+
type: "object",
|
|
146
|
+
additionalProperties: false,
|
|
147
|
+
properties: {
|
|
148
|
+
duration: { $ref: "#/$defs/duration" },
|
|
149
|
+
warmup: { $ref: "#/$defs/duration" },
|
|
150
|
+
load: { $ref: "#/$defs/load" },
|
|
151
|
+
signing: { $ref: "#/$defs/signingMode" },
|
|
152
|
+
signatureTimeWindow: { type: "boolean" },
|
|
153
|
+
runs: {
|
|
154
|
+
type: "integer",
|
|
155
|
+
minimum: 1
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
actorGroup: {
|
|
160
|
+
type: "object",
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
required: ["signatureStandards"],
|
|
163
|
+
properties: {
|
|
164
|
+
name: { type: "string" },
|
|
165
|
+
count: {
|
|
166
|
+
type: "integer",
|
|
167
|
+
minimum: 1
|
|
168
|
+
},
|
|
169
|
+
signatureStandards: {
|
|
170
|
+
type: "array",
|
|
171
|
+
uniqueItems: true,
|
|
172
|
+
minItems: 1,
|
|
173
|
+
items: { $ref: "#/$defs/signatureStandard" },
|
|
174
|
+
contains: { enum: ["draft-cavage-http-signatures-12", "rfc9421"] },
|
|
175
|
+
minContains: 1,
|
|
176
|
+
maxContains: 1,
|
|
177
|
+
description: "Exactly one HTTP request signature scheme, plus optional document signature schemes."
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
generateDirective: {
|
|
182
|
+
type: "object",
|
|
183
|
+
additionalProperties: false,
|
|
184
|
+
required: ["generate"],
|
|
185
|
+
properties: {
|
|
186
|
+
generate: { enum: ["lorem"] },
|
|
187
|
+
size: { $ref: "#/$defs/size" }
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
content: { oneOf: [{ type: "string" }, { $ref: "#/$defs/generateDirective" }] },
|
|
191
|
+
objectSpec: {
|
|
192
|
+
type: "object",
|
|
193
|
+
properties: {
|
|
194
|
+
type: { $ref: "#/$defs/scalarOrListString" },
|
|
195
|
+
content: { $ref: "#/$defs/content" }
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
activitySpec: {
|
|
199
|
+
type: "object",
|
|
200
|
+
additionalProperties: false,
|
|
201
|
+
properties: {
|
|
202
|
+
type: { $ref: "#/$defs/scalarOrListString" },
|
|
203
|
+
embedObject: { type: "boolean" },
|
|
204
|
+
object: { $ref: "#/$defs/objectSpec" }
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
objectSource: { oneOf: [{ $ref: "#/$defs/scalarOrListString" }, {
|
|
208
|
+
type: "object",
|
|
209
|
+
additionalProperties: false,
|
|
210
|
+
required: ["seed"],
|
|
211
|
+
properties: {
|
|
212
|
+
seed: { $ref: "#/$defs/scalarOrListString" },
|
|
213
|
+
collection: { $ref: "#/$defs/scalarOrListString" },
|
|
214
|
+
limit: {
|
|
215
|
+
type: "integer",
|
|
216
|
+
minimum: 1
|
|
217
|
+
},
|
|
218
|
+
type: { $ref: "#/$defs/scalarOrListString" }
|
|
219
|
+
}
|
|
220
|
+
}] },
|
|
221
|
+
expectSeverity: { enum: ["warn", "fail"] },
|
|
222
|
+
expectValue: { oneOf: [{
|
|
223
|
+
type: "string",
|
|
224
|
+
description: "An assertion such as '>= 99%' or '< 100ms'."
|
|
225
|
+
}, {
|
|
226
|
+
type: "object",
|
|
227
|
+
additionalProperties: false,
|
|
228
|
+
required: ["assert"],
|
|
229
|
+
properties: {
|
|
230
|
+
assert: { type: "string" },
|
|
231
|
+
severity: { $ref: "#/$defs/expectSeverity" }
|
|
232
|
+
}
|
|
233
|
+
}] },
|
|
234
|
+
mixEntry: {
|
|
235
|
+
type: "object",
|
|
236
|
+
additionalProperties: false,
|
|
237
|
+
required: ["scenario", "weight"],
|
|
238
|
+
properties: {
|
|
239
|
+
scenario: { type: "string" },
|
|
240
|
+
weight: {
|
|
241
|
+
type: "number",
|
|
242
|
+
exclusiveMinimum: 0
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
scenario: {
|
|
247
|
+
type: "object",
|
|
248
|
+
additionalProperties: false,
|
|
249
|
+
required: ["name", "type"],
|
|
250
|
+
properties: {
|
|
251
|
+
name: { type: "string" },
|
|
252
|
+
type: { enum: [
|
|
253
|
+
"inbox",
|
|
254
|
+
"webfinger",
|
|
255
|
+
"actor",
|
|
256
|
+
"object",
|
|
257
|
+
"fanout",
|
|
258
|
+
"collection",
|
|
259
|
+
"failure",
|
|
260
|
+
"mixed"
|
|
261
|
+
] },
|
|
262
|
+
load: { $ref: "#/$defs/load" },
|
|
263
|
+
duration: { $ref: "#/$defs/duration" },
|
|
264
|
+
warmup: { $ref: "#/$defs/duration" },
|
|
265
|
+
signing: { $ref: "#/$defs/signingMode" },
|
|
266
|
+
signatureTimeWindow: { type: "boolean" },
|
|
267
|
+
runs: {
|
|
268
|
+
type: "integer",
|
|
269
|
+
minimum: 1
|
|
270
|
+
},
|
|
271
|
+
expect: {
|
|
272
|
+
type: "object",
|
|
273
|
+
additionalProperties: { $ref: "#/$defs/expectValue" }
|
|
274
|
+
},
|
|
275
|
+
recipient: { $ref: "#/$defs/scalarOrListString" },
|
|
276
|
+
inbox: { type: "string" },
|
|
277
|
+
activity: { $ref: "#/$defs/activitySpec" },
|
|
278
|
+
authenticated: { type: "boolean" },
|
|
279
|
+
collection: { $ref: "#/$defs/scalarOrListString" },
|
|
280
|
+
source: { $ref: "#/$defs/objectSource" },
|
|
281
|
+
sender: { type: "string" },
|
|
282
|
+
followers: {
|
|
283
|
+
type: "integer",
|
|
284
|
+
minimum: 1
|
|
285
|
+
},
|
|
286
|
+
trigger: { type: "object" },
|
|
287
|
+
sinkBehavior: { type: "object" },
|
|
288
|
+
queueDrainTimeout: { $ref: "#/$defs/duration" },
|
|
289
|
+
fault: { $ref: "#/$defs/scalarOrListString" },
|
|
290
|
+
mix: {
|
|
291
|
+
type: "array",
|
|
292
|
+
minItems: 1,
|
|
293
|
+
items: { $ref: "#/$defs/mixEntry" }
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
allOf: [
|
|
297
|
+
{
|
|
298
|
+
if: { properties: { type: { const: "inbox" } } },
|
|
299
|
+
then: {
|
|
300
|
+
required: ["recipient"],
|
|
301
|
+
properties: { expect: { propertyNames: { enum: INBOX_METRICS } } }
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
if: { properties: { type: { const: "webfinger" } } },
|
|
306
|
+
then: {
|
|
307
|
+
required: ["recipient"],
|
|
308
|
+
properties: { expect: { propertyNames: { enum: READ_METRICS } } }
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
if: { properties: { type: { const: "actor" } } },
|
|
313
|
+
then: {
|
|
314
|
+
required: ["recipient"],
|
|
315
|
+
properties: { expect: { propertyNames: { enum: INBOX_METRICS } } }
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
if: { properties: { type: { const: "object" } } },
|
|
320
|
+
then: {
|
|
321
|
+
required: ["source"],
|
|
322
|
+
properties: { expect: { propertyNames: { enum: READ_METRICS } } }
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
if: { properties: { type: { const: "collection" } } },
|
|
327
|
+
then: {
|
|
328
|
+
required: ["recipient"],
|
|
329
|
+
properties: { expect: { propertyNames: { enum: READ_METRICS } } }
|
|
330
|
+
}
|
|
331
|
+
},
|
|
332
|
+
{
|
|
333
|
+
if: { properties: { type: { const: "fanout" } } },
|
|
334
|
+
then: {
|
|
335
|
+
required: ["sender"],
|
|
336
|
+
properties: { expect: { propertyNames: { enum: FANOUT_METRICS } } }
|
|
337
|
+
}
|
|
338
|
+
},
|
|
339
|
+
{
|
|
340
|
+
if: { properties: { type: { const: "failure" } } },
|
|
341
|
+
then: {
|
|
342
|
+
required: ["fault"],
|
|
343
|
+
properties: { expect: { propertyNames: { enum: READ_METRICS } } }
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
if: { properties: { type: { const: "mixed" } } },
|
|
348
|
+
then: {
|
|
349
|
+
required: ["mix"],
|
|
350
|
+
properties: { expect: { propertyNames: { enum: MIXED_METRICS } } }
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
]
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
//#endregion
|
|
358
|
+
export { scenarioSchemaV1 };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import "@js-temporal/polyfill";
|
|
2
|
+
//#region src/bench/scenario/units.ts
|
|
3
|
+
/**
|
|
4
|
+
* Parsers for the human-friendly duration and rate units used in scenario
|
|
5
|
+
* files.
|
|
6
|
+
* @since 2.3.0
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
const DURATION_RE = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/;
|
|
10
|
+
const DURATION_UNITS = {
|
|
11
|
+
ms: 1,
|
|
12
|
+
s: 1e3,
|
|
13
|
+
m: 6e4,
|
|
14
|
+
h: 36e5
|
|
15
|
+
};
|
|
16
|
+
const RATE_RE = /^(\d+(?:\.\d+)?)\s*\/\s*(s|m|h)$/;
|
|
17
|
+
const RATE_DIVISORS = {
|
|
18
|
+
s: 1,
|
|
19
|
+
m: 60,
|
|
20
|
+
h: 3600
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Parses a duration such as `"500ms"`, `"30s"`, `"2m"`, or `"1h"` into
|
|
24
|
+
* milliseconds.
|
|
25
|
+
* @param value The duration string.
|
|
26
|
+
* @returns The duration in milliseconds.
|
|
27
|
+
* @throws {RangeError} If the value cannot be parsed.
|
|
28
|
+
*/
|
|
29
|
+
function parseDuration(value) {
|
|
30
|
+
const match = value.match(DURATION_RE);
|
|
31
|
+
if (match == null) throw new RangeError(`Invalid duration: ${JSON.stringify(value)}.`);
|
|
32
|
+
const ms = Number.parseFloat(match[1]) * DURATION_UNITS[match[2]];
|
|
33
|
+
if (!Number.isFinite(ms)) throw new RangeError(`Duration out of range: ${JSON.stringify(value)}.`);
|
|
34
|
+
return ms;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Parses an open-loop arrival rate into requests per second. A bare number is
|
|
38
|
+
* interpreted as requests per second; a string such as `"200/s"`, `"60/m"`, or
|
|
39
|
+
* `"3600/h"` carries an explicit time unit.
|
|
40
|
+
* @param value The rate string or number.
|
|
41
|
+
* @returns The rate in requests per second.
|
|
42
|
+
* @throws {RangeError} If the value cannot be parsed or is not positive.
|
|
43
|
+
*/
|
|
44
|
+
function parseRate(value) {
|
|
45
|
+
if (typeof value === "number") {
|
|
46
|
+
if (!Number.isFinite(value) || value <= 0) throw new RangeError(`Invalid rate: ${value}.`);
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
const match = value.match(RATE_RE);
|
|
50
|
+
if (match == null) throw new RangeError(`Invalid rate: ${JSON.stringify(value)}.`);
|
|
51
|
+
const rate = Number.parseFloat(match[1]) / RATE_DIVISORS[match[2]];
|
|
52
|
+
if (!Number.isFinite(rate) || rate <= 0) throw new RangeError(`Invalid rate: ${JSON.stringify(value)}.`);
|
|
53
|
+
return rate;
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { parseDuration, parseRate };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import "@js-temporal/polyfill";
|
|
2
|
+
import { scenarioSchemaV1 } from "./schema.js";
|
|
3
|
+
import { SuiteValidationError } from "./errors.js";
|
|
4
|
+
import { Validator } from "@cfworker/json-schema";
|
|
5
|
+
//#region src/bench/scenario/validate.ts
|
|
6
|
+
/**
|
|
7
|
+
* Runtime validation of scenario suites against the embedded JSON Schema.
|
|
8
|
+
* @since 2.3.0
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
let validator;
|
|
12
|
+
function getValidator() {
|
|
13
|
+
validator ??= new Validator(scenarioSchemaV1, "2020-12");
|
|
14
|
+
return validator;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Validates a parsed scenario suite against the schema and narrows its type.
|
|
18
|
+
* @param raw The parsed (but untyped) suite value.
|
|
19
|
+
* @param source An optional source label (e.g. a file path) for error messages.
|
|
20
|
+
* @returns The validated suite.
|
|
21
|
+
* @throws {SuiteValidationError} If the value does not satisfy the schema.
|
|
22
|
+
*/
|
|
23
|
+
function validateSuite(raw, source) {
|
|
24
|
+
const result = getValidator().validate(raw);
|
|
25
|
+
if (!result.valid) throw new SuiteValidationError(result.errors, source);
|
|
26
|
+
return raw;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
export { validateSuite };
|