@glubean/runner 0.7.0 → 0.8.0
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/engine-bridge.d.ts +4 -0
- package/dist/engine-bridge.d.ts.map +1 -1
- package/dist/engine-bridge.js +10 -1
- package/dist/engine-bridge.js.map +1 -1
- package/dist/executor.d.ts +2 -2
- package/dist/executor.d.ts.map +1 -1
- package/dist/executor.js +9 -226
- package/dist/executor.js.map +1 -1
- package/dist/harness.js +3 -79
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/load/continuation-pool.d.ts +82 -0
- package/dist/load/continuation-pool.d.ts.map +1 -0
- package/dist/load/continuation-pool.js +154 -0
- package/dist/load/continuation-pool.js.map +1 -0
- package/dist/load/execute-iteration.d.ts +126 -0
- package/dist/load/execute-iteration.d.ts.map +1 -0
- package/dist/load/execute-iteration.js +367 -0
- package/dist/load/execute-iteration.js.map +1 -0
- package/dist/load/histogram.d.ts +63 -0
- package/dist/load/histogram.d.ts.map +1 -0
- package/dist/load/histogram.js +149 -0
- package/dist/load/histogram.js.map +1 -0
- package/dist/load/orchestrator.d.ts +55 -0
- package/dist/load/orchestrator.d.ts.map +1 -0
- package/dist/load/orchestrator.js +571 -0
- package/dist/load/orchestrator.js.map +1 -0
- package/dist/load/reducer.d.ts +109 -0
- package/dist/load/reducer.d.ts.map +1 -0
- package/dist/load/reducer.js +718 -0
- package/dist/load/reducer.js.map +1 -0
- package/dist/load/route-key.d.ts +38 -0
- package/dist/load/route-key.d.ts.map +1 -0
- package/dist/load/route-key.js +107 -0
- package/dist/load/route-key.js.map +1 -0
- package/dist/load/samples.d.ts +83 -0
- package/dist/load/samples.d.ts.map +1 -0
- package/dist/load/samples.js +269 -0
- package/dist/load/samples.js.map +1 -0
- package/dist/load/sink.d.ts +127 -0
- package/dist/load/sink.d.ts.map +1 -0
- package/dist/load/sink.js +351 -0
- package/dist/load/sink.js.map +1 -0
- package/dist/load/subprocess.d.ts +83 -0
- package/dist/load/subprocess.d.ts.map +1 -0
- package/dist/load/subprocess.js +229 -0
- package/dist/load/subprocess.js.map +1 -0
- package/dist/load/threshold.d.ts +44 -0
- package/dist/load/threshold.d.ts.map +1 -0
- package/dist/load/threshold.js +197 -0
- package/dist/load/threshold.js.map +1 -0
- package/dist/load/timeline.d.ts +36 -0
- package/dist/load/timeline.d.ts.map +1 -0
- package/dist/load/timeline.js +158 -0
- package/dist/load/timeline.js.map +1 -0
- package/dist/load-harness.d.ts +2 -0
- package/dist/load-harness.d.ts.map +1 -0
- package/dist/load-harness.js +105 -0
- package/dist/load-harness.js.map +1 -0
- package/dist/runner-resolve.d.ts +53 -0
- package/dist/runner-resolve.d.ts.map +1 -0
- package/dist/runner-resolve.js +264 -0
- package/dist/runner-resolve.js.map +1 -0
- package/dist/workflow/event-timeline.d.ts +3 -0
- package/dist/workflow/event-timeline.d.ts.map +1 -0
- package/dist/workflow/event-timeline.js +72 -0
- package/dist/workflow/event-timeline.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subprocess load execution — spawn the runner's `load-harness` in a child
|
|
3
|
+
* process so the harness (which calls `runLoad`) and the user `.load.ts` file
|
|
4
|
+
* CO-RESOLVE the same `@glubean/sdk`, mirroring how `glubean run` spawns the
|
|
5
|
+
* project-local test harness.
|
|
6
|
+
*
|
|
7
|
+
* Why a subprocess at all: running `runLoad` in-process against a user file that
|
|
8
|
+
* resolves a DIFFERENT `@glubean/sdk` instance (a globally-installed CLI vs. a
|
|
9
|
+
* project with its own, non-deduped sdk) split-brains the SDK runtime carrier —
|
|
10
|
+
* the scenario reads one AsyncLocalStorage carrier while `runLoad` installs
|
|
11
|
+
* another. Spawning the project-local runner via tsx makes both halves resolve
|
|
12
|
+
* the project's sdk, eliminating the hazard.
|
|
13
|
+
*
|
|
14
|
+
* This module owns the PARENT side (`runLoadFileInSubprocess`) plus the small
|
|
15
|
+
* pure helpers the child `load-harness` shares (`collectLoadPlans`,
|
|
16
|
+
* `withProcessEnvFallback`) and the wire-message contract between them
|
|
17
|
+
* (`WIRE_PREFIX` / `LoadHarnessMessage`) — kept here as a single source of truth
|
|
18
|
+
* so producer and consumer can't drift.
|
|
19
|
+
*/
|
|
20
|
+
import { spawn } from "node:child_process";
|
|
21
|
+
import { existsSync } from "node:fs";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
import { dirname, resolve } from "node:path";
|
|
24
|
+
import { resolveRunnerRoot, resolveTsxPath, prepareZeroProject } from "../runner-resolve.js";
|
|
25
|
+
// ── Shared wire contract (child stdout → parent) ─────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Marker prefixed to every harness protocol line on stdout. The user's load file
|
|
28
|
+
* + plugins share stdout, whose `console.log` may itself print JSON; gating on
|
|
29
|
+
* this token stops a stray `{"type":"error",...}` log from spoofing an outcome or
|
|
30
|
+
* a failure. It leads with the ASCII Unit Separator (0x1F) — a control byte that
|
|
31
|
+
* ordinary text logs never emit — so collision is negligible. Lines without it
|
|
32
|
+
* are forwarded to the parent's stdout as ordinary user output.
|
|
33
|
+
*
|
|
34
|
+
* (stdout, not a dedicated fd: tsx re-spawns an inner node process and forwards
|
|
35
|
+
* only fd 0/1/2, so an extra protocol fd wouldn't reach the harness.)
|
|
36
|
+
*/
|
|
37
|
+
export const WIRE_PREFIX = "\x1fglubean-load-wire\x1f";
|
|
38
|
+
// ── Shared pure helpers (used by load-harness in the child) ──────────────────
|
|
39
|
+
/** A LoadPlan-shaped export: the runnable marker the orchestrator consumes. */
|
|
40
|
+
function isLoadPlan(value) {
|
|
41
|
+
return (typeof value === "object" &&
|
|
42
|
+
value !== null &&
|
|
43
|
+
value.__glubean_type === "load-runner");
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Collect every LoadPlan exported by a module namespace (flattening `.each()`
|
|
47
|
+
* arrays of plans).
|
|
48
|
+
*/
|
|
49
|
+
export function collectLoadPlans(ns) {
|
|
50
|
+
const plans = [];
|
|
51
|
+
for (const value of Object.values(ns)) {
|
|
52
|
+
if (isLoadPlan(value)) {
|
|
53
|
+
plans.push(value);
|
|
54
|
+
}
|
|
55
|
+
else if (Array.isArray(value)) {
|
|
56
|
+
for (const v of value)
|
|
57
|
+
if (isLoadPlan(v))
|
|
58
|
+
plans.push(v);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return plans;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Wrap an env map so a missing key falls back to `process.env` — the same
|
|
65
|
+
* shell/CI env semantics the test harness gives `ctx.vars` / `ctx.secrets`, so
|
|
66
|
+
* `BASE_URL=... glubean load ...` resolves even without a `.env` entry.
|
|
67
|
+
*
|
|
68
|
+
* Applied INSIDE the child (which inherits the parent's `process.env`): the
|
|
69
|
+
* parent ships the raw resolved `{ vars, secrets }` over stdin — a Proxy can't
|
|
70
|
+
* survive JSON serialization, so the fallback must be re-established where
|
|
71
|
+
* `process.env` is actually present. Spreading the proxy (for `ctx.vars.all()`)
|
|
72
|
+
* still yields only the explicit map's own keys, not the whole environment.
|
|
73
|
+
*/
|
|
74
|
+
export function withProcessEnvFallback(map) {
|
|
75
|
+
// Parity with the test harness's env proxy: an empty/nullish map value is
|
|
76
|
+
// treated as MISSING and falls back to process.env (so `BASE_URL=` in .env plus
|
|
77
|
+
// `BASE_URL=... glubean load` resolves), and process.env empties read as unset.
|
|
78
|
+
return new Proxy(map, {
|
|
79
|
+
get(target, prop) {
|
|
80
|
+
if (typeof prop === "string") {
|
|
81
|
+
const value = target[prop];
|
|
82
|
+
if (value !== undefined && value !== null && value !== "")
|
|
83
|
+
return value;
|
|
84
|
+
return process.env[prop] || undefined;
|
|
85
|
+
}
|
|
86
|
+
return Reflect.get(target, prop);
|
|
87
|
+
},
|
|
88
|
+
has(target, prop) {
|
|
89
|
+
return Reflect.has(target, prop) || (typeof prop === "string" && process.env[prop] !== undefined);
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
94
|
+
// This module builds to dist/load/subprocess.js; load-harness.js and the runner
|
|
95
|
+
// package root sit two and three levels up respectively.
|
|
96
|
+
const BUNDLED_DIST_DIR = resolve(__dirname, "..");
|
|
97
|
+
const BUNDLED_PKG_ROOT = resolve(__dirname, "..", "..");
|
|
98
|
+
/** Parse one prefixed protocol line into a wire message, or null if malformed. */
|
|
99
|
+
function parseHarnessLine(line) {
|
|
100
|
+
const trimmed = line.trim();
|
|
101
|
+
if (!trimmed)
|
|
102
|
+
return null;
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(trimmed);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null; // partial / malformed protocol line — ignore
|
|
109
|
+
}
|
|
110
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
111
|
+
return null;
|
|
112
|
+
const msg = parsed;
|
|
113
|
+
if (msg.type === "artifact" && typeof msg.runnerId === "string" && msg.artifact && typeof msg.artifact === "object") {
|
|
114
|
+
return { type: "artifact", runnerId: msg.runnerId, artifact: msg.artifact };
|
|
115
|
+
}
|
|
116
|
+
if (msg.type === "error" && typeof msg.message === "string") {
|
|
117
|
+
return { type: "error", message: msg.message };
|
|
118
|
+
}
|
|
119
|
+
if (msg.type === "done")
|
|
120
|
+
return { type: "done" };
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Run one `.load.ts` file's plans in a child process and collect their
|
|
125
|
+
* artifacts. Spawns `load-harness.js` (project-local when the project ships a
|
|
126
|
+
* built runner with it, else the CLI's bundled copy) through tsx so the harness
|
|
127
|
+
* and the user file resolve the same `@glubean/sdk`. The raw `{ vars, secrets }`
|
|
128
|
+
* are written to the child's stdin; the child applies the process.env fallback.
|
|
129
|
+
*
|
|
130
|
+
* Mirrors the in-process contract: a file's import failure or a plan's run
|
|
131
|
+
* failure becomes an `errors[]` entry (never throws), so other files/plans still
|
|
132
|
+
* produce results.
|
|
133
|
+
*/
|
|
134
|
+
export async function runLoadFileInSubprocess(file, opts) {
|
|
135
|
+
const { vars, secrets, cwd } = opts;
|
|
136
|
+
// Resolve the runner the same way `glubean run` does (dual-package hazard fix).
|
|
137
|
+
const resolved = resolveRunnerRoot(cwd, BUNDLED_DIST_DIR, BUNDLED_PKG_ROOT);
|
|
138
|
+
let distDir = resolved.distDir;
|
|
139
|
+
let pkgRoot = resolved.pkgRoot;
|
|
140
|
+
let harnessPath = resolve(distDir, "load-harness.js");
|
|
141
|
+
if (!existsSync(harnessPath)) {
|
|
142
|
+
// The resolved runner predates load support (has harness.js but no
|
|
143
|
+
// load-harness.js). Fall back to the bundled harness so load still runs.
|
|
144
|
+
// (Re-introduces the in-process split-brain only when the project ALSO ships
|
|
145
|
+
// its own non-deduped sdk against a too-old runner — no worse than before,
|
|
146
|
+
// and the common up-to-date case is fully correct. Upgrading the project's
|
|
147
|
+
// @glubean/runner restores shared module identity.)
|
|
148
|
+
distDir = BUNDLED_DIST_DIR;
|
|
149
|
+
pkgRoot = BUNDLED_PKG_ROOT;
|
|
150
|
+
harnessPath = resolve(BUNDLED_DIST_DIR, "load-harness.js");
|
|
151
|
+
}
|
|
152
|
+
const zp = prepareZeroProject(cwd, distDir, pkgRoot);
|
|
153
|
+
try {
|
|
154
|
+
const env = { ...process.env, ...zp.env };
|
|
155
|
+
const child = spawn("node", [resolveTsxPath(), ...zp.tsxArgs, harnessPath, `--file=${file}`], {
|
|
156
|
+
cwd,
|
|
157
|
+
env,
|
|
158
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
159
|
+
});
|
|
160
|
+
child.stdin.write(JSON.stringify({ vars, secrets }));
|
|
161
|
+
child.stdin.end();
|
|
162
|
+
const outcomes = [];
|
|
163
|
+
const errors = [];
|
|
164
|
+
let stdoutBuffer = "";
|
|
165
|
+
let sawDone = false;
|
|
166
|
+
const stderrChunks = [];
|
|
167
|
+
const consumeLine = (line) => {
|
|
168
|
+
if (!line.startsWith(WIRE_PREFIX)) {
|
|
169
|
+
// Ordinary user/plugin output — forward it so nothing is swallowed.
|
|
170
|
+
if (line.length > 0)
|
|
171
|
+
process.stdout.write(line + "\n");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const msg = parseHarnessLine(line.slice(WIRE_PREFIX.length));
|
|
175
|
+
if (!msg)
|
|
176
|
+
return;
|
|
177
|
+
if (msg.type === "artifact")
|
|
178
|
+
outcomes.push({ runnerId: msg.runnerId, artifact: msg.artifact });
|
|
179
|
+
else if (msg.type === "error")
|
|
180
|
+
errors.push({ message: msg.message });
|
|
181
|
+
else
|
|
182
|
+
sawDone = true; // terminal sentinel: the harness finished cleanly
|
|
183
|
+
};
|
|
184
|
+
child.stdout.on("data", (chunk) => {
|
|
185
|
+
stdoutBuffer += chunk.toString();
|
|
186
|
+
const lines = stdoutBuffer.split("\n");
|
|
187
|
+
stdoutBuffer = lines.pop() ?? "";
|
|
188
|
+
for (const line of lines)
|
|
189
|
+
consumeLine(line);
|
|
190
|
+
});
|
|
191
|
+
// Forward child stderr LIVE so user/plugin diagnostics (console.error) stay
|
|
192
|
+
// visible as they did in-process, and keep a copy for the crash message.
|
|
193
|
+
child.stderr.on("data", (chunk) => {
|
|
194
|
+
stderrChunks.push(chunk);
|
|
195
|
+
process.stderr.write(chunk);
|
|
196
|
+
});
|
|
197
|
+
await new Promise((resolveClose) => {
|
|
198
|
+
child.on("error", (err) => {
|
|
199
|
+
errors.push({
|
|
200
|
+
message: err.code === "ENOENT"
|
|
201
|
+
? "NODE_NOT_FOUND: Node.js is not installed. Glubean requires Node.js 20+ to run load plans."
|
|
202
|
+
: `failed to start load subprocess for ${file}: ${err.message}`,
|
|
203
|
+
});
|
|
204
|
+
resolveClose();
|
|
205
|
+
});
|
|
206
|
+
child.on("close", (code, signal) => {
|
|
207
|
+
if (stdoutBuffer)
|
|
208
|
+
consumeLine(stdoutBuffer);
|
|
209
|
+
// No `done` sentinel ⇒ the child died before finishing (crash / OOM /
|
|
210
|
+
// SIGKILL), even if it already emitted some artifacts. Surface a crash
|
|
211
|
+
// error so a PARTIAL run is never mistaken for a complete one. (A clean
|
|
212
|
+
// import-failure path still emits `done`, so it never lands here.)
|
|
213
|
+
if (!sawDone) {
|
|
214
|
+
const stderr = Buffer.concat(stderrChunks).toString().trim();
|
|
215
|
+
const how = signal ? `signal ${signal}` : `exit code ${code}`;
|
|
216
|
+
errors.push({
|
|
217
|
+
message: `load subprocess for ${file} did not complete (${how})${stderr ? `:\n${stderr}` : ""}`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
resolveClose();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
return { outcomes, errors };
|
|
224
|
+
}
|
|
225
|
+
finally {
|
|
226
|
+
zp.cleanup();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=subprocess.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subprocess.js","sourceRoot":"","sources":["../../src/load/subprocess.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured threshold parsing + evaluation (M4-a).
|
|
3
|
+
*
|
|
4
|
+
* A `loadRunner({ thresholds })` config carries human expressions per scope/metric
|
|
5
|
+
* (`errorRate: "<1%"`, `p95: "<800ms"`, `throughputPerSec: ">100/s"`). This module
|
|
6
|
+
* parses those expressions, evaluates them against a finalized `LoadArtifact`, and
|
|
7
|
+
* produces the `ThresholdEvaluation[]` + a refined pass verdict (a crash-free run
|
|
8
|
+
* only passes if every configured-and-evaluable threshold holds).
|
|
9
|
+
*
|
|
10
|
+
* Evaluation runs OUTSIDE the reducer (which never sees the plan's thresholds) as a
|
|
11
|
+
* post-finalize step in the orchestrator. Scopes whose data isn't present yet
|
|
12
|
+
* (primary / endToEnd / continuation phase splits — M5/M6) and metrics N/A to a
|
|
13
|
+
* scope (e.g. throughput on a step) are skipped rather than failed.
|
|
14
|
+
*/
|
|
15
|
+
import type { LoadArtifact, LoadThresholds, ThresholdEvaluation } from "@glubean/sdk/load";
|
|
16
|
+
type Op = "<" | "<=" | ">" | ">=";
|
|
17
|
+
/** Metrics a threshold can target (the keys of `LoadThresholdScope`). */
|
|
18
|
+
declare const THRESHOLD_METRICS: readonly ["errorRate", "p50", "p90", "p95", "p99", "throughputPerSec", "backlog", "backpressureMs"];
|
|
19
|
+
type ThresholdMetric = (typeof THRESHOLD_METRICS)[number];
|
|
20
|
+
/** A parsed threshold: comparison operator + the value in the metric's base unit. */
|
|
21
|
+
export interface ParsedThreshold {
|
|
22
|
+
op: Op;
|
|
23
|
+
/** Value in the metric's base unit: errorRate=fraction, latency/backpressure=ms,
|
|
24
|
+
* throughput=/s, backlog=count. */
|
|
25
|
+
value: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse one threshold expression for a given metric, normalizing its value to the
|
|
29
|
+
* metric's base unit (`%`→fraction, `s`→ms, etc.). Throws on a malformed
|
|
30
|
+
* expression or a unit that doesn't fit the metric.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseThresholdExpression(expr: string, metric: ThresholdMetric): ParsedThreshold;
|
|
33
|
+
/**
|
|
34
|
+
* Evaluate every configured threshold against the artifact, returning the
|
|
35
|
+
* `ThresholdEvaluation[]` and the refined run pass (crash-free AND every evaluable
|
|
36
|
+
* threshold holds). Thresholds whose scope data is absent or whose metric is N/A
|
|
37
|
+
* to the scope are skipped (not failed).
|
|
38
|
+
*/
|
|
39
|
+
export declare function evaluateThresholds(artifact: LoadArtifact, thresholds: LoadThresholds): {
|
|
40
|
+
thresholds: ThresholdEvaluation[];
|
|
41
|
+
pass: boolean;
|
|
42
|
+
};
|
|
43
|
+
export {};
|
|
44
|
+
//# sourceMappingURL=threshold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threshold.d.ts","sourceRoot":"","sources":["../../src/load/threshold.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EACV,YAAY,EAEZ,cAAc,EACd,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAE3B,KAAK,EAAE,GAAG,GAAG,GAAG,IAAI,GAAG,GAAG,GAAG,IAAI,CAAC;AAElC,yEAAyE;AACzE,QAAA,MAAM,iBAAiB,qGASb,CAAC;AACX,KAAK,eAAe,GAAG,CAAC,OAAO,iBAAiB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1D,qFAAqF;AACrF,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,EAAE,CAAC;IACP;wCACoC;IACpC,KAAK,EAAE,MAAM,CAAC;CACf;AAID;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAG,eAAe,CAqC/F;AA+FD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,EACtB,UAAU,EAAE,cAAc,GACzB;IAAE,UAAU,EAAE,mBAAmB,EAAE,CAAC;IAAC,IAAI,EAAE,OAAO,CAAA;CAAE,CAgFtD"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/** Metrics a threshold can target (the keys of `LoadThresholdScope`). */
|
|
2
|
+
const THRESHOLD_METRICS = [
|
|
3
|
+
"errorRate",
|
|
4
|
+
"p50",
|
|
5
|
+
"p90",
|
|
6
|
+
"p95",
|
|
7
|
+
"p99",
|
|
8
|
+
"throughputPerSec",
|
|
9
|
+
"backlog",
|
|
10
|
+
"backpressureMs",
|
|
11
|
+
];
|
|
12
|
+
const EXPR_RE = /^(<=|>=|<|>)\s*(-?[0-9.]+)\s*(%|ms|s|\/s)?$/;
|
|
13
|
+
/**
|
|
14
|
+
* Parse one threshold expression for a given metric, normalizing its value to the
|
|
15
|
+
* metric's base unit (`%`→fraction, `s`→ms, etc.). Throws on a malformed
|
|
16
|
+
* expression or a unit that doesn't fit the metric.
|
|
17
|
+
*/
|
|
18
|
+
export function parseThresholdExpression(expr, metric) {
|
|
19
|
+
const m = EXPR_RE.exec(expr.trim());
|
|
20
|
+
if (!m) {
|
|
21
|
+
throw new Error(`invalid threshold expression ${JSON.stringify(expr)} — expected e.g. "<1%", "<800ms", ">100/s"`);
|
|
22
|
+
}
|
|
23
|
+
const op = m[1];
|
|
24
|
+
const num = Number(m[2]);
|
|
25
|
+
const unit = m[3];
|
|
26
|
+
if (!Number.isFinite(num))
|
|
27
|
+
throw new Error(`invalid threshold number in ${JSON.stringify(expr)}`);
|
|
28
|
+
const badUnit = () => {
|
|
29
|
+
throw new Error(`threshold unit "${unit}" is not valid for metric "${metric}" (in ${JSON.stringify(expr)})`);
|
|
30
|
+
};
|
|
31
|
+
let value;
|
|
32
|
+
switch (metric) {
|
|
33
|
+
case "errorRate":
|
|
34
|
+
// A bare number is already a fraction (0..1); "%" scales a percentage down.
|
|
35
|
+
value = unit === "%" ? num / 100 : unit === undefined ? num : badUnit();
|
|
36
|
+
break;
|
|
37
|
+
case "p50":
|
|
38
|
+
case "p90":
|
|
39
|
+
case "p95":
|
|
40
|
+
case "p99":
|
|
41
|
+
case "backpressureMs":
|
|
42
|
+
value = unit === "s" ? num * 1000 : unit === "ms" || unit === undefined ? num : badUnit();
|
|
43
|
+
break;
|
|
44
|
+
case "throughputPerSec":
|
|
45
|
+
value = unit === "/s" || unit === undefined ? num : badUnit();
|
|
46
|
+
break;
|
|
47
|
+
case "backlog":
|
|
48
|
+
value = unit === undefined ? num : badUnit();
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
return { op, value };
|
|
52
|
+
}
|
|
53
|
+
function compare(actual, op, value) {
|
|
54
|
+
switch (op) {
|
|
55
|
+
case "<":
|
|
56
|
+
return actual < value;
|
|
57
|
+
case "<=":
|
|
58
|
+
return actual <= value;
|
|
59
|
+
case ">":
|
|
60
|
+
return actual > value;
|
|
61
|
+
case ">=":
|
|
62
|
+
return actual >= value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Combine an endpoint's / step's per-phase rows (M5 splits a route or step hit in
|
|
67
|
+
* both phases into two rows) into ONE scope for thresholding:
|
|
68
|
+
* - throughput is ADDITIVE → summed (60/s primary + 60/s continuation = 120/s);
|
|
69
|
+
* - errorRate is weighted by each row's denominator (Σ rate·weight / Σ weight), so
|
|
70
|
+
* it stays request-based for endpoints and invocation-based for steps;
|
|
71
|
+
* - percentiles can't be merged from summaries, so each is the MAX across rows —
|
|
72
|
+
* exact for the usual `<` upper-bound threshold (every row under X ⟺ max under X
|
|
73
|
+
* ⟹ the merged distribution is under X), conservative otherwise.
|
|
74
|
+
* A single row (the no-split common case) combines to its own values unchanged.
|
|
75
|
+
*/
|
|
76
|
+
function combineRows(rows) {
|
|
77
|
+
let errorWeighted = 0;
|
|
78
|
+
let errorWeight = 0;
|
|
79
|
+
let throughput = 0;
|
|
80
|
+
let hasThroughput = false;
|
|
81
|
+
const latency = { p50: 0, p90: 0, p95: 0, p99: 0 };
|
|
82
|
+
for (const r of rows) {
|
|
83
|
+
errorWeighted += r.errorRate * r.errorWeight;
|
|
84
|
+
errorWeight += r.errorWeight;
|
|
85
|
+
if (r.throughputPerSec !== undefined) {
|
|
86
|
+
throughput += r.throughputPerSec;
|
|
87
|
+
hasThroughput = true;
|
|
88
|
+
}
|
|
89
|
+
latency.p50 = Math.max(latency.p50, r.latency.p50);
|
|
90
|
+
latency.p90 = Math.max(latency.p90, r.latency.p90);
|
|
91
|
+
latency.p95 = Math.max(latency.p95, r.latency.p95);
|
|
92
|
+
latency.p99 = Math.max(latency.p99, r.latency.p99);
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
errorRate: errorWeight > 0 ? errorWeighted / errorWeight : 0,
|
|
96
|
+
...(hasThroughput ? { throughputPerSec: throughput } : {}),
|
|
97
|
+
latency,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
/** Pull the actual value for `metric` from a scope's data, or undefined if N/A. */
|
|
101
|
+
function actualFor(metric, data) {
|
|
102
|
+
switch (metric) {
|
|
103
|
+
case "errorRate":
|
|
104
|
+
return data.errorRate;
|
|
105
|
+
case "p50":
|
|
106
|
+
return data.latency?.p50;
|
|
107
|
+
case "p90":
|
|
108
|
+
return data.latency?.p90;
|
|
109
|
+
case "p95":
|
|
110
|
+
return data.latency?.p95;
|
|
111
|
+
case "p99":
|
|
112
|
+
return data.latency?.p99;
|
|
113
|
+
case "throughputPerSec":
|
|
114
|
+
return data.throughputPerSec;
|
|
115
|
+
case "backlog":
|
|
116
|
+
return data.backlog;
|
|
117
|
+
case "backpressureMs":
|
|
118
|
+
return data.backpressureMs;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Evaluate every configured threshold against the artifact, returning the
|
|
123
|
+
* `ThresholdEvaluation[]` and the refined run pass (crash-free AND every evaluable
|
|
124
|
+
* threshold holds). Thresholds whose scope data is absent or whose metric is N/A
|
|
125
|
+
* to the scope are skipped (not failed).
|
|
126
|
+
*/
|
|
127
|
+
export function evaluateThresholds(artifact, thresholds) {
|
|
128
|
+
const out = [];
|
|
129
|
+
const evalScope = (scope, target, data, cfg) => {
|
|
130
|
+
if (!data || !cfg)
|
|
131
|
+
return;
|
|
132
|
+
for (const metric of THRESHOLD_METRICS) {
|
|
133
|
+
const expr = cfg[metric];
|
|
134
|
+
if (expr === undefined)
|
|
135
|
+
continue;
|
|
136
|
+
const actual = actualFor(metric, data);
|
|
137
|
+
if (actual === undefined)
|
|
138
|
+
continue; // metric not applicable to this scope
|
|
139
|
+
const { op, value } = parseThresholdExpression(expr, metric);
|
|
140
|
+
out.push({
|
|
141
|
+
scope,
|
|
142
|
+
...(target !== undefined ? { target } : {}),
|
|
143
|
+
metric,
|
|
144
|
+
expression: expr,
|
|
145
|
+
actual,
|
|
146
|
+
pass: compare(actual, op, value),
|
|
147
|
+
source: "glubean",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
const s = artifact.summary;
|
|
152
|
+
evalScope("transaction", undefined, { errorRate: s.errorRate, latency: s.latency, throughputPerSec: s.throughputPerSec }, thresholds.transaction);
|
|
153
|
+
evalScope("primary", undefined, s.primary, thresholds.primary);
|
|
154
|
+
evalScope("endToEnd", undefined, s.endToEnd, thresholds.endToEnd);
|
|
155
|
+
// The continuation summary needs numeric projection: `backpressureMs` is a
|
|
156
|
+
// percentile distribution (compare against p95), and the threshold-relevant
|
|
157
|
+
// backlog is the PEAK (`maxBacklog`), not the drained-to-0 live count.
|
|
158
|
+
const continuationData = s.continuation
|
|
159
|
+
? { backlog: s.continuation.maxBacklog, backpressureMs: s.continuation.backpressureMs?.p95 ?? 0 }
|
|
160
|
+
: undefined;
|
|
161
|
+
evalScope("continuation", undefined, continuationData, thresholds.continuation);
|
|
162
|
+
// Endpoints / steps can split into per-phase rows (M5: a route or step hit in
|
|
163
|
+
// both the primary and continuation phase appears twice). Combine the matching
|
|
164
|
+
// rows into one scope so additive metrics (throughput) sum and a slow phase still
|
|
165
|
+
// shows up (latency = max) — picking one row (e.g. `find`) would let a slow
|
|
166
|
+
// continuation hide behind a fast primary. No matching row → skipped, not failed.
|
|
167
|
+
if (thresholds.endpoints) {
|
|
168
|
+
for (const [routeKey, cfg] of Object.entries(thresholds.endpoints)) {
|
|
169
|
+
const eps = artifact.endpoints.filter((e) => e.routeKey === routeKey);
|
|
170
|
+
// Endpoint error rate is over REQUESTS.
|
|
171
|
+
const rows = eps.map((e) => ({
|
|
172
|
+
errorRate: e.errorRate,
|
|
173
|
+
errorWeight: e.requestCount,
|
|
174
|
+
throughputPerSec: e.throughputPerSec,
|
|
175
|
+
latency: e.latency,
|
|
176
|
+
}));
|
|
177
|
+
evalScope("endpoint", routeKey, rows.length > 0 ? combineRows(rows) : undefined, cfg);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (thresholds.steps) {
|
|
181
|
+
for (const [stepId, cfg] of Object.entries(thresholds.steps)) {
|
|
182
|
+
const sts = artifact.steps.filter((x) => x.stepId === stepId);
|
|
183
|
+
// Step error rate is over EXECUTED invocations (skipped ones aren't failures);
|
|
184
|
+
// steps have no throughput metric.
|
|
185
|
+
const rows = sts.map((s) => ({
|
|
186
|
+
errorRate: s.errorRate,
|
|
187
|
+
errorWeight: Math.max(0, s.invocationCount - s.skippedCount),
|
|
188
|
+
latency: s.latency,
|
|
189
|
+
}));
|
|
190
|
+
evalScope("step", stepId, rows.length > 0 ? combineRows(rows) : undefined, cfg);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// A crash already fails the run; otherwise every evaluable threshold must hold.
|
|
194
|
+
const pass = s.pass && out.every((e) => e.pass);
|
|
195
|
+
return { thresholds: out, pass };
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=threshold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"threshold.js","sourceRoot":"","sources":["../../src/load/threshold.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { LoadTimeline as LoadTimelineArtifact } from "@glubean/sdk/load";
|
|
2
|
+
export declare class LoadTimeline {
|
|
3
|
+
private readonly baseWindowMs;
|
|
4
|
+
private readonly maxWindows;
|
|
5
|
+
private windowMs;
|
|
6
|
+
private windows;
|
|
7
|
+
/** @param baseWindowMs initial window width (ms). @param maxWindows coarsening cap. */
|
|
8
|
+
constructor(baseWindowMs?: number, maxWindows?: number);
|
|
9
|
+
/** The window index covering `offsetMs` (ms from run start), coarsening first if the
|
|
10
|
+
* index would exceed the cap (so the series stays ≤ maxWindows). */
|
|
11
|
+
private indexFor;
|
|
12
|
+
/** The LAST window covered by a run of length `runEndMs` (the run spans [0, runEndMs)), or
|
|
13
|
+
* -1 for a non-positive length. A run ending exactly on a boundary (runEndMs = k·windowMs)
|
|
14
|
+
* covers windows 0..k-1, NOT a window that starts at the run end (codex). Coarsens so the
|
|
15
|
+
* index fits the cap. */
|
|
16
|
+
private runEndIndex;
|
|
17
|
+
/** The window covering `offsetMs`, creating it if absent. */
|
|
18
|
+
private windowFor;
|
|
19
|
+
/** Double the window width and merge each adjacent pair (2k, 2k+1 → k). All folds are
|
|
20
|
+
* commutative (sum / max / histogram merge), so Map iteration order is irrelevant. */
|
|
21
|
+
private coarsen;
|
|
22
|
+
/** Record one request observation at `offsetMs`. `inFlight` is the live iteration count
|
|
23
|
+
* at that moment — sampled so a window busy with requests (e.g. a poll) shows concurrency. */
|
|
24
|
+
recordRequest(offsetMs: number, durationMs: number, ok: boolean, inFlight: number): void;
|
|
25
|
+
/** Record one started iteration at `offsetMs`. `inFlight` is the live count just after the
|
|
26
|
+
* start (its local peak), sampled so even a same-window start+end shows its concurrency. */
|
|
27
|
+
recordIterationStart(offsetMs: number, inFlight: number): void;
|
|
28
|
+
/** Record one completed iteration (iteration:end) at `offsetMs`. */
|
|
29
|
+
recordIterationEnd(offsetMs: number): void;
|
|
30
|
+
/** Emit the dense series: every window from 0..last, idle windows zero-filled. `runEndMs`
|
|
31
|
+
* (the run's `load:end` offset) extends the series to the actual run end, so a trailing
|
|
32
|
+
* idle / sustained-in-flight period after the last recorded event (an abort, drain timeout,
|
|
33
|
+
* or hung iteration) is still present instead of being truncated (codex). */
|
|
34
|
+
finalize(runEndMs?: number): LoadTimelineArtifact;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=timeline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../../src/load/timeline.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,IAAI,oBAAoB,EAAmC,MAAM,mBAAmB,CAAC;AAoB/G,qBAAa,YAAY;IAQrB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAR7B,OAAO,CAAC,QAAQ,CAAS;IAGzB,OAAO,CAAC,OAAO,CAA6B;IAE5C,uFAAuF;gBAEpE,YAAY,SAAM,EAClB,UAAU,SAAM;IAKnC;yEACqE;IACrE,OAAO,CAAC,QAAQ;IAShB;;;8BAG0B;IAC1B,OAAO,CAAC,WAAW;IAMnB,6DAA6D;IAC7D,OAAO,CAAC,SAAS;IAUjB;2FACuF;IACvF,OAAO,CAAC,OAAO;IAoBf;mGAC+F;IAC/F,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAQxF;iGAC6F;IAC7F,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAM9D,oEAAoE;IACpE,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;IAI1C;;;kFAG8E;IAC9E,QAAQ,CAAC,QAAQ,SAAI,GAAG,oBAAoB;CA2C7C"}
|