@untestutils/perf 0.5.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/LICENSE +21 -0
- package/dist/average.d.mts +4 -0
- package/dist/average.d.ts +5 -0
- package/dist/average.d.ts.map +1 -0
- package/dist/average.mjs +92 -0
- package/dist/build.d.mts +2 -0
- package/dist/build.d.ts +3 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.mjs +69 -0
- package/dist/bundle.d.mts +2 -0
- package/dist/bundle.d.ts +3 -0
- package/dist/bundle.d.ts.map +1 -0
- package/dist/bundle.mjs +42 -0
- package/dist/define.d.mts +2 -0
- package/dist/define.d.ts +3 -0
- package/dist/define.d.ts.map +1 -0
- package/dist/define.mjs +3 -0
- package/dist/format.d.mts +2 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.mjs +15 -0
- package/dist/index.d.mts +17 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +15 -0
- package/dist/load/artillery.d.mts +10 -0
- package/dist/load/artillery.d.ts +11 -0
- package/dist/load/artillery.d.ts.map +1 -0
- package/dist/load/artillery.mjs +77 -0
- package/dist/load/autocannon.d.mts +9 -0
- package/dist/load/autocannon.d.ts +10 -0
- package/dist/load/autocannon.d.ts.map +1 -0
- package/dist/load/autocannon.mjs +96 -0
- package/dist/load/index.d.mts +7 -0
- package/dist/load/index.d.ts +8 -0
- package/dist/load/index.d.ts.map +1 -0
- package/dist/load/index.mjs +48 -0
- package/dist/load/parse.d.mts +5 -0
- package/dist/load/parse.d.ts +6 -0
- package/dist/load/parse.d.ts.map +1 -0
- package/dist/load/parse.mjs +8 -0
- package/dist/process-sample.d.ts +31 -0
- package/dist/process-sample.d.ts.map +1 -0
- package/dist/process-sample.mjs +64 -0
- package/dist/report/console.d.mts +2 -0
- package/dist/report/console.d.ts +3 -0
- package/dist/report/console.d.ts.map +1 -0
- package/dist/report/console.mjs +25 -0
- package/dist/report/json.d.mts +4 -0
- package/dist/report/json.d.ts +5 -0
- package/dist/report/json.d.ts.map +1 -0
- package/dist/report/json.mjs +19 -0
- package/dist/run.d.mts +5 -0
- package/dist/run.d.ts +6 -0
- package/dist/run.d.ts.map +1 -0
- package/dist/run.mjs +88 -0
- package/dist/start.d.ts +12 -0
- package/dist/start.d.ts.map +1 -0
- package/dist/start.mjs +114 -0
- package/dist/thresholds.d.mts +8 -0
- package/dist/thresholds.d.ts +9 -0
- package/dist/thresholds.d.ts.map +1 -0
- package/dist/thresholds.mjs +48 -0
- package/dist/types.d.mts +172 -0
- package/dist/types.d.ts +173 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.mjs +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 s00d
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { BuildMetrics, LoadMetrics, PerfTargetResult } from "./types.mjs";
|
|
2
|
+
export declare function averageBuildMetrics(results: BuildMetrics[]): BuildMetrics;
|
|
3
|
+
export declare function averageLoadMetrics(results: LoadMetrics[]): LoadMetrics;
|
|
4
|
+
export declare function averageTargetResults(results: PerfTargetResult[]): PerfTargetResult;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { BuildMetrics, LoadMetrics, PerfTargetResult } from './types';
|
|
2
|
+
export declare function averageBuildMetrics(results: BuildMetrics[]): BuildMetrics;
|
|
3
|
+
export declare function averageLoadMetrics(results: LoadMetrics[]): LoadMetrics;
|
|
4
|
+
export declare function averageTargetResults(results: PerfTargetResult[]): PerfTargetResult;
|
|
5
|
+
//# sourceMappingURL=average.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"average.d.ts","sourceRoot":"","sources":["../src/average.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,YAAY,EAEZ,WAAW,EACX,gBAAgB,EACjB,MAAM,SAAS,CAAC;AAoDjB,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,YAAY,CAYzE;AAED,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,WAAW,EAAE,GAAG,WAAW,CAsBtE;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,CAUlF"}
|
package/dist/average.mjs
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
function avg(values) {
|
|
2
|
+
const nums = values.filter((v) => typeof v === "number" && Number.isFinite(v));
|
|
3
|
+
if (!nums.length) return undefined;
|
|
4
|
+
return nums.reduce((a, b) => a + b, 0) / nums.length;
|
|
5
|
+
}
|
|
6
|
+
function averageBundle(sizes) {
|
|
7
|
+
const present = sizes.filter((s) => Boolean(s));
|
|
8
|
+
if (!present.length) return undefined;
|
|
9
|
+
const byDir = {};
|
|
10
|
+
for (const s of present) {
|
|
11
|
+
for (const [k, v] of Object.entries(s.byDir)) {
|
|
12
|
+
byDir[k] = (byDir[k] ?? 0) + v;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
for (const k of Object.keys(byDir)) byDir[k] /= present.length;
|
|
16
|
+
return {
|
|
17
|
+
total: avg(present.map((s) => s.total)) ?? 0,
|
|
18
|
+
code: avg(present.map((s) => s.code)) ?? 0,
|
|
19
|
+
asset: avg(present.map((s) => s.asset)) ?? 0,
|
|
20
|
+
other: avg(present.map((s) => s.other)) ?? 0,
|
|
21
|
+
byDir
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function averageAutocannon(items) {
|
|
25
|
+
const present = items.filter((a) => Boolean(a));
|
|
26
|
+
if (!present.length) return undefined;
|
|
27
|
+
return {
|
|
28
|
+
requests: {
|
|
29
|
+
average: avg(present.map((a) => a.requests.average)) ?? 0,
|
|
30
|
+
mean: avg(present.map((a) => a.requests.mean)) ?? 0,
|
|
31
|
+
total: avg(present.map((a) => a.requests.total)) ?? 0
|
|
32
|
+
},
|
|
33
|
+
latency: {
|
|
34
|
+
average: avg(present.map((a) => a.latency.average)) ?? 0,
|
|
35
|
+
mean: avg(present.map((a) => a.latency.mean)) ?? 0,
|
|
36
|
+
min: avg(present.map((a) => a.latency.min)) ?? 0,
|
|
37
|
+
max: avg(present.map((a) => a.latency.max)) ?? 0,
|
|
38
|
+
p50: avg(present.map((a) => a.latency.p50)) ?? 0,
|
|
39
|
+
p97_5: avg(present.map((a) => a.latency.p97_5)) ?? 0,
|
|
40
|
+
p99: avg(present.map((a) => a.latency.p99)) ?? 0
|
|
41
|
+
},
|
|
42
|
+
throughput: { average: avg(present.map((a) => a.throughput.average)) ?? 0 },
|
|
43
|
+
errors: avg(present.map((a) => a.errors)) ?? 0
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export function averageBuildMetrics(results) {
|
|
47
|
+
if (!results.length) throw new Error("cannot average empty build metrics");
|
|
48
|
+
return {
|
|
49
|
+
buildTimeSec: avg(results.map((r) => r.buildTimeSec)) ?? 0,
|
|
50
|
+
maxMemoryMb: avg(results.map((r) => r.maxMemoryMb)) ?? 0,
|
|
51
|
+
minMemoryMb: avg(results.map((r) => r.minMemoryMb)) ?? 0,
|
|
52
|
+
avgMemoryMb: avg(results.map((r) => r.avgMemoryMb)) ?? 0,
|
|
53
|
+
maxCpuPct: avg(results.map((r) => r.maxCpuPct)) ?? 0,
|
|
54
|
+
minCpuPct: avg(results.map((r) => r.minCpuPct)) ?? 0,
|
|
55
|
+
avgCpuPct: avg(results.map((r) => r.avgCpuPct)) ?? 0,
|
|
56
|
+
bundle: averageBundle(results.map((r) => r.bundle))
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function averageLoadMetrics(results) {
|
|
60
|
+
if (!results.length) throw new Error("cannot average empty load metrics");
|
|
61
|
+
const last = results[results.length - 1];
|
|
62
|
+
return {
|
|
63
|
+
maxMemoryMb: avg(results.map((r) => r.maxMemoryMb)) ?? 0,
|
|
64
|
+
minMemoryMb: avg(results.map((r) => r.minMemoryMb)) ?? 0,
|
|
65
|
+
avgMemoryMb: avg(results.map((r) => r.avgMemoryMb)) ?? 0,
|
|
66
|
+
maxCpuPct: avg(results.map((r) => r.maxCpuPct)) ?? 0,
|
|
67
|
+
minCpuPct: avg(results.map((r) => r.minCpuPct)) ?? 0,
|
|
68
|
+
avgCpuPct: avg(results.map((r) => r.avgCpuPct)) ?? 0,
|
|
69
|
+
durationSec: avg(results.map((r) => r.durationSec)),
|
|
70
|
+
responseTimeAvg: avg(results.map((r) => r.responseTimeAvg)),
|
|
71
|
+
responseTimeMin: avg(results.map((r) => r.responseTimeMin)),
|
|
72
|
+
responseTimeMax: avg(results.map((r) => r.responseTimeMax)),
|
|
73
|
+
responseTimeP50: avg(results.map((r) => r.responseTimeP50)),
|
|
74
|
+
responseTimeP95: avg(results.map((r) => r.responseTimeP95)),
|
|
75
|
+
responseTimeP99: avg(results.map((r) => r.responseTimeP99)),
|
|
76
|
+
requestsPerSecond: avg(results.map((r) => r.requestsPerSecond)),
|
|
77
|
+
errorRate: avg(results.map((r) => r.errorRate)),
|
|
78
|
+
autocannon: averageAutocannon(results.map((r) => r.autocannon)),
|
|
79
|
+
artillery: last.artillery
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export function averageTargetResults(results) {
|
|
83
|
+
if (!results.length) throw new Error("cannot average empty target results");
|
|
84
|
+
const first = results[0];
|
|
85
|
+
const loads = results.map((r) => r.load).filter((l) => Boolean(l));
|
|
86
|
+
return {
|
|
87
|
+
id: first.id,
|
|
88
|
+
label: first.label,
|
|
89
|
+
build: averageBuildMetrics(results.map((r) => r.build)),
|
|
90
|
+
load: loads.length === results.length ? averageLoadMetrics(loads) : undefined
|
|
91
|
+
};
|
|
92
|
+
}
|
package/dist/build.d.mts
ADDED
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAKxD,wBAAsB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,YAAY,CAAC,CAiE5E"}
|
package/dist/build.mjs
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { performance } from "node:perf_hooks";
|
|
3
|
+
import { resolve } from "pathe";
|
|
4
|
+
import { measureBundle } from "./bundle.mjs";
|
|
5
|
+
import { finalizeSamples, startProcessMonitor } from "./process-sample.mjs";
|
|
6
|
+
const defaultLogFilter = (line) => /Building|built|ERROR|WARN|preset|complete|Nitro|Client|Server|✓|✔/i.test(line);
|
|
7
|
+
export async function measureBuild(target) {
|
|
8
|
+
const cwd = resolve(target.build.cwd ?? target.root);
|
|
9
|
+
const args = target.build.args ?? [];
|
|
10
|
+
const env = {
|
|
11
|
+
PATH: process.env.PATH,
|
|
12
|
+
HOME: process.env.HOME,
|
|
13
|
+
USER: process.env.USER,
|
|
14
|
+
SHELL: process.env.SHELL,
|
|
15
|
+
NODE_ENV: "production",
|
|
16
|
+
...target.build.env
|
|
17
|
+
};
|
|
18
|
+
const started = performance.now();
|
|
19
|
+
const child = spawn(target.build.command, args, {
|
|
20
|
+
cwd,
|
|
21
|
+
env,
|
|
22
|
+
stdio: [
|
|
23
|
+
"ignore",
|
|
24
|
+
"pipe",
|
|
25
|
+
"pipe"
|
|
26
|
+
]
|
|
27
|
+
});
|
|
28
|
+
const filter = target.build.logFilter ?? defaultLogFilter;
|
|
29
|
+
child.stdout?.on("data", (buf) => {
|
|
30
|
+
for (const line of buf.toString().split("\n")) {
|
|
31
|
+
const t = line.trim();
|
|
32
|
+
if (t && filter(t)) console.log(` [build] ${t.slice(0, 200)}`);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
child.stderr?.on("data", (buf) => {
|
|
36
|
+
const t = buf.toString().trim();
|
|
37
|
+
if (t) console.error(` [build stderr] ${t.slice(0, 240)}`);
|
|
38
|
+
});
|
|
39
|
+
const monitor = child.pid ? startProcessMonitor(child.pid, 1e3, (s) => {
|
|
40
|
+
|
|
41
|
+
void s;
|
|
42
|
+
}) : undefined;
|
|
43
|
+
try {
|
|
44
|
+
await new Promise((resolvePromise, reject) => {
|
|
45
|
+
child.on("close", (code) => {
|
|
46
|
+
if (code === 0) resolvePromise();
|
|
47
|
+
else reject(new Error(`Build exited with code ${code}`));
|
|
48
|
+
});
|
|
49
|
+
child.on("error", reject);
|
|
50
|
+
});
|
|
51
|
+
} finally {
|
|
52
|
+
monitor?.stop();
|
|
53
|
+
}
|
|
54
|
+
const buildTimeSec = (performance.now() - started) / 1e3;
|
|
55
|
+
const processMetrics = monitor ? finalizeSamples(monitor.acc) : {
|
|
56
|
+
maxMemoryMb: 0,
|
|
57
|
+
minMemoryMb: 0,
|
|
58
|
+
avgMemoryMb: 0,
|
|
59
|
+
maxCpuPct: 0,
|
|
60
|
+
minCpuPct: 0,
|
|
61
|
+
avgCpuPct: 0
|
|
62
|
+
};
|
|
63
|
+
const bundle = target.bundle ? measureBundle(resolve(target.root), target.bundle) : undefined;
|
|
64
|
+
return {
|
|
65
|
+
buildTimeSec,
|
|
66
|
+
...processMetrics,
|
|
67
|
+
bundle
|
|
68
|
+
};
|
|
69
|
+
}
|
package/dist/bundle.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../src/bundle.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAe,UAAU,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAmBzE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,UAAU,CAyB9E"}
|
package/dist/bundle.mjs
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, join } from "pathe";
|
|
3
|
+
function walk(dir, classify, into) {
|
|
4
|
+
if (!existsSync(dir)) return;
|
|
5
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
6
|
+
const full = join(dir, entry.name);
|
|
7
|
+
if (entry.isDirectory()) walk(full, classify, into);
|
|
8
|
+
else {
|
|
9
|
+
const size = statSync(full).size;
|
|
10
|
+
const kind = classify(full);
|
|
11
|
+
into[kind] += size;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function measureBundle(root, opts) {
|
|
16
|
+
const classify = opts.classify ?? (() => "code");
|
|
17
|
+
const byDir = {};
|
|
18
|
+
let code = 0;
|
|
19
|
+
let asset = 0;
|
|
20
|
+
let other = 0;
|
|
21
|
+
for (const rel of opts.dirs) {
|
|
22
|
+
const dir = rel.startsWith("/") ? rel : join(root, rel);
|
|
23
|
+
const bucket = {
|
|
24
|
+
code: 0,
|
|
25
|
+
asset: 0,
|
|
26
|
+
other: 0
|
|
27
|
+
};
|
|
28
|
+
walk(dir, classify, bucket);
|
|
29
|
+
const name = basename(dir);
|
|
30
|
+
byDir[name] = bucket.code + bucket.asset + bucket.other;
|
|
31
|
+
code += bucket.code;
|
|
32
|
+
asset += bucket.asset;
|
|
33
|
+
other += bucket.other;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
total: code + asset + other,
|
|
37
|
+
code,
|
|
38
|
+
asset,
|
|
39
|
+
other,
|
|
40
|
+
byDir
|
|
41
|
+
};
|
|
42
|
+
}
|
package/dist/define.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define.d.ts","sourceRoot":"","sources":["../src/define.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,CAE3D"}
|
package/dist/define.mjs
ADDED
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAMjD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C"}
|
package/dist/format.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function formatBytes(bytes) {
|
|
2
|
+
if (bytes === 0) return "0 B";
|
|
3
|
+
const k = 1024;
|
|
4
|
+
const sizes = [
|
|
5
|
+
"B",
|
|
6
|
+
"KB",
|
|
7
|
+
"MB",
|
|
8
|
+
"GB"
|
|
9
|
+
];
|
|
10
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
11
|
+
return `${Number.parseFloat((bytes / k ** i).toFixed(2))} ${sizes[i]}`;
|
|
12
|
+
}
|
|
13
|
+
export function formatSec(sec) {
|
|
14
|
+
return `${sec.toFixed(2)}s`;
|
|
15
|
+
}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export { definePerfSuite } from "./define.mjs";
|
|
2
|
+
export { runPerfSuite } from "./run.mjs";
|
|
3
|
+
export { measureBuild } from "./build.mjs";
|
|
4
|
+
export { measureBundle } from "./bundle.mjs";
|
|
5
|
+
export { startTarget, isPortFree } from "./start.mjs";
|
|
6
|
+
export type { StartedTarget } from "./start.mjs";
|
|
7
|
+
export { runLoadPhase } from "./load/index.mjs";
|
|
8
|
+
export { runAutocannon } from "./load/autocannon.mjs";
|
|
9
|
+
export { runArtillery } from "./load/artillery.mjs";
|
|
10
|
+
export { parseAutocannonJson, parseArtilleryJson } from "./load/parse.mjs";
|
|
11
|
+
export { averageTargetResults, averageBuildMetrics, averageLoadMetrics } from "./average.mjs";
|
|
12
|
+
export { checkThresholds } from "./thresholds.mjs";
|
|
13
|
+
export { formatBytes, formatSec } from "./format.mjs";
|
|
14
|
+
export { sampleProcess, startProcessMonitor } from "./process-sample.mjs";
|
|
15
|
+
export { consoleReporter } from "./report/console.mjs";
|
|
16
|
+
export { jsonReporter } from "./report/json.mjs";
|
|
17
|
+
export type * from "./types.mjs";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { definePerfSuite } from './define';
|
|
2
|
+
export { runPerfSuite } from './run';
|
|
3
|
+
export { measureBuild } from './build';
|
|
4
|
+
export { measureBundle } from './bundle';
|
|
5
|
+
export { startTarget, isPortFree } from './start';
|
|
6
|
+
export type { StartedTarget } from './start';
|
|
7
|
+
export { runLoadPhase } from './load';
|
|
8
|
+
export { runAutocannon } from './load/autocannon';
|
|
9
|
+
export { runArtillery } from './load/artillery';
|
|
10
|
+
export { parseAutocannonJson, parseArtilleryJson } from './load/parse';
|
|
11
|
+
export { averageTargetResults, averageBuildMetrics, averageLoadMetrics } from './average';
|
|
12
|
+
export { checkThresholds } from './thresholds';
|
|
13
|
+
export { formatBytes, formatSec } from './format';
|
|
14
|
+
export { sampleProcess, startProcessMonitor } from './process-sample';
|
|
15
|
+
export { consoleReporter } from './report/console';
|
|
16
|
+
export { jsonReporter } from './report/json';
|
|
17
|
+
export type * from './types';
|
|
18
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AACrC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAClD,YAAY,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC1F,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,mBAAmB,SAAS,CAAC"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export { definePerfSuite } from "./define.mjs";
|
|
2
|
+
export { runPerfSuite } from "./run.mjs";
|
|
3
|
+
export { measureBuild } from "./build.mjs";
|
|
4
|
+
export { measureBundle } from "./bundle.mjs";
|
|
5
|
+
export { startTarget, isPortFree } from "./start.mjs";
|
|
6
|
+
export { runLoadPhase } from "./load/index.mjs";
|
|
7
|
+
export { runAutocannon } from "./load/autocannon.mjs";
|
|
8
|
+
export { runArtillery } from "./load/artillery.mjs";
|
|
9
|
+
export { parseAutocannonJson, parseArtilleryJson } from "./load/parse.mjs";
|
|
10
|
+
export { averageTargetResults, averageBuildMetrics, averageLoadMetrics } from "./average.mjs";
|
|
11
|
+
export { checkThresholds } from "./thresholds.mjs";
|
|
12
|
+
export { formatBytes, formatSec } from "./format.mjs";
|
|
13
|
+
export { sampleProcess, startProcessMonitor } from "./process-sample.mjs";
|
|
14
|
+
export { consoleReporter } from "./report/console.mjs";
|
|
15
|
+
export { jsonReporter } from "./report/json.mjs";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ArtilleryResult } from "../types.mjs";
|
|
2
|
+
/** Prefer local artillery bin; fall back to `npx artillery`. */
|
|
3
|
+
export declare function runArtillery(opts: {
|
|
4
|
+
configPath: string;
|
|
5
|
+
artifactsDir: string;
|
|
6
|
+
name: string;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
/** Overrides `config.target` (needed when the suite picks a free port). */
|
|
9
|
+
targetUrl?: string;
|
|
10
|
+
}): Promise<ArtilleryResult>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ArtilleryResult } from '../types';
|
|
2
|
+
/** Prefer local artillery bin; fall back to `npx artillery`. */
|
|
3
|
+
export declare function runArtillery(opts: {
|
|
4
|
+
configPath: string;
|
|
5
|
+
artifactsDir: string;
|
|
6
|
+
name: string;
|
|
7
|
+
cwd?: string;
|
|
8
|
+
/** Overrides `config.target` (needed when the suite picks a free port). */
|
|
9
|
+
targetUrl?: string;
|
|
10
|
+
}): Promise<ArtilleryResult>;
|
|
11
|
+
//# sourceMappingURL=artillery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artillery.d.ts","sourceRoot":"","sources":["../../src/load/artillery.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAoDhD,gEAAgE;AAChE,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,eAAe,CAAC,CAW3B"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "pathe";
|
|
5
|
+
function resolveArtilleryBin() {
|
|
6
|
+
try {
|
|
7
|
+
const pkg = createRequire(import.meta.url).resolve("artillery/package.json");
|
|
8
|
+
const bin = join(dirname(pkg), "bin", "run");
|
|
9
|
+
return existsSync(bin) ? bin : null;
|
|
10
|
+
} catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function spawnArtillery(args, opts) {
|
|
15
|
+
return new Promise((resolvePromise, reject) => {
|
|
16
|
+
const child = spawn(args[0], args.slice(1), {
|
|
17
|
+
cwd: opts.cwd,
|
|
18
|
+
stdio: [
|
|
19
|
+
"ignore",
|
|
20
|
+
"pipe",
|
|
21
|
+
"pipe"
|
|
22
|
+
]
|
|
23
|
+
});
|
|
24
|
+
child.stdout?.on("data", (chunk) => {
|
|
25
|
+
const line = chunk.toString().trim();
|
|
26
|
+
if (line.includes("Phase started") || line.includes("Summary report") || line.includes("All VUs finished")) {
|
|
27
|
+
console.log(` [artillery] ${line.split("\n")[0]}`);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
child.stderr?.on("data", (chunk) => {
|
|
31
|
+
const line = chunk.toString().trim();
|
|
32
|
+
if (line) console.error(` [artillery stderr] ${line.slice(0, 200)}`);
|
|
33
|
+
});
|
|
34
|
+
child.on("error", reject);
|
|
35
|
+
child.on("close", (code) => {
|
|
36
|
+
if (code !== 0) {
|
|
37
|
+
reject(new Error(`Artillery exited with code ${code}`));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
resolvePromise(JSON.parse(readFileSync(opts.outputFile, "utf8")));
|
|
42
|
+
} catch (err) {
|
|
43
|
+
reject(err);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function runArtillery(opts) {
|
|
50
|
+
mkdirSync(opts.artifactsDir, { recursive: true });
|
|
51
|
+
const outputFile = join(opts.artifactsDir, `artillery-${opts.name}.json`);
|
|
52
|
+
const config = resolve(opts.configPath);
|
|
53
|
+
const bin = resolveArtilleryBin();
|
|
54
|
+
const targetArgs = opts.targetUrl ? ["--target", opts.targetUrl.replace(/\/$/, "")] : [];
|
|
55
|
+
const args = bin ? [
|
|
56
|
+
process.execPath,
|
|
57
|
+
bin,
|
|
58
|
+
"run",
|
|
59
|
+
...targetArgs,
|
|
60
|
+
config,
|
|
61
|
+
"--output",
|
|
62
|
+
outputFile
|
|
63
|
+
] : [
|
|
64
|
+
"npx",
|
|
65
|
+
"--yes",
|
|
66
|
+
"artillery",
|
|
67
|
+
"run",
|
|
68
|
+
...targetArgs,
|
|
69
|
+
config,
|
|
70
|
+
"--output",
|
|
71
|
+
outputFile
|
|
72
|
+
];
|
|
73
|
+
return spawnArtillery(args, {
|
|
74
|
+
cwd: opts.cwd,
|
|
75
|
+
outputFile
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AutocannonResult } from "../types.mjs";
|
|
2
|
+
/** Prefer programmatic peer; fall back to `npx autocannon`. */
|
|
3
|
+
export declare function runAutocannon(opts: {
|
|
4
|
+
url: string;
|
|
5
|
+
connections?: number;
|
|
6
|
+
durationSec?: number;
|
|
7
|
+
artifactsDir: string;
|
|
8
|
+
name: string;
|
|
9
|
+
}): Promise<AutocannonResult>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AutocannonResult } from '../types';
|
|
2
|
+
/** Prefer programmatic peer; fall back to `npx autocannon`. */
|
|
3
|
+
export declare function runAutocannon(opts: {
|
|
4
|
+
url: string;
|
|
5
|
+
connections?: number;
|
|
6
|
+
durationSec?: number;
|
|
7
|
+
artifactsDir: string;
|
|
8
|
+
name: string;
|
|
9
|
+
}): Promise<AutocannonResult>;
|
|
10
|
+
//# sourceMappingURL=autocannon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"autocannon.d.ts","sourceRoot":"","sources":["../../src/load/autocannon.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAiHjD,+DAA+D;AAC/D,wBAAsB,aAAa,CAAC,IAAI,EAAE;IACxC,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CACd,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAc5B"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "pathe";
|
|
4
|
+
function normalize(raw) {
|
|
5
|
+
return {
|
|
6
|
+
requests: {
|
|
7
|
+
average: raw.requests.average,
|
|
8
|
+
mean: raw.requests.mean ?? raw.requests.average,
|
|
9
|
+
total: raw.requests.total ?? raw.totalCompletedRequests ?? raw.totalRequests ?? 0
|
|
10
|
+
},
|
|
11
|
+
latency: {
|
|
12
|
+
average: raw.latency.average,
|
|
13
|
+
mean: raw.latency.mean ?? raw.latency.average,
|
|
14
|
+
min: raw.latency.min,
|
|
15
|
+
max: raw.latency.max,
|
|
16
|
+
p50: raw.latency.p50 ?? raw.latency.average,
|
|
17
|
+
p97_5: raw.latency.p97_5 ?? raw.latency.p99 ?? raw.latency.max,
|
|
18
|
+
p99: raw.latency.p99 ?? raw.latency.max
|
|
19
|
+
},
|
|
20
|
+
throughput: { average: raw.throughput.average },
|
|
21
|
+
errors: raw.errors ?? 0
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
async function viaApi(opts) {
|
|
25
|
+
try {
|
|
26
|
+
// @ts-expect-error optional peer
|
|
27
|
+
const mod = await import("autocannon");
|
|
28
|
+
const run = mod.default ?? mod;
|
|
29
|
+
const raw = await run({
|
|
30
|
+
url: opts.url,
|
|
31
|
+
connections: opts.connections,
|
|
32
|
+
duration: opts.duration,
|
|
33
|
+
renderProgressBar: false,
|
|
34
|
+
renderResultsTable: false,
|
|
35
|
+
renderLatencyTable: false
|
|
36
|
+
});
|
|
37
|
+
return normalize(raw);
|
|
38
|
+
} catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function viaNpx(opts) {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const child = spawn("npx", [
|
|
45
|
+
"--yes",
|
|
46
|
+
"autocannon",
|
|
47
|
+
"-c",
|
|
48
|
+
String(opts.connections),
|
|
49
|
+
"-d",
|
|
50
|
+
String(opts.duration),
|
|
51
|
+
"-j",
|
|
52
|
+
opts.url
|
|
53
|
+
], { stdio: [
|
|
54
|
+
"ignore",
|
|
55
|
+
"pipe",
|
|
56
|
+
"pipe"
|
|
57
|
+
] });
|
|
58
|
+
let stdout = "";
|
|
59
|
+
let stderr = "";
|
|
60
|
+
child.stdout?.on("data", (c) => {
|
|
61
|
+
stdout += c.toString();
|
|
62
|
+
});
|
|
63
|
+
child.stderr?.on("data", (c) => {
|
|
64
|
+
stderr += c.toString();
|
|
65
|
+
});
|
|
66
|
+
child.on("error", reject);
|
|
67
|
+
child.on("close", (code) => {
|
|
68
|
+
if (code !== 0 && !stdout) {
|
|
69
|
+
reject(new Error(`autocannon exited ${code}: ${stderr.slice(0, 400)}`));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
resolve(normalize(JSON.parse(stdout)));
|
|
74
|
+
} catch (e) {
|
|
75
|
+
reject(e);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function runAutocannon(opts) {
|
|
82
|
+
const connections = opts.connections ?? 10;
|
|
83
|
+
const duration = opts.durationSec ?? 10;
|
|
84
|
+
mkdirSync(opts.artifactsDir, { recursive: true });
|
|
85
|
+
const result = await viaApi({
|
|
86
|
+
url: opts.url,
|
|
87
|
+
connections,
|
|
88
|
+
duration
|
|
89
|
+
}) ?? await viaNpx({
|
|
90
|
+
url: opts.url,
|
|
91
|
+
connections,
|
|
92
|
+
duration
|
|
93
|
+
});
|
|
94
|
+
writeFileSync(join(opts.artifactsDir, `autocannon-${opts.name}.json`), JSON.stringify(result, null, 2));
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { StartedTarget } from '../start';
|
|
2
|
+
import type { LoadMetrics, PerfTarget } from '../types';
|
|
3
|
+
export declare function runLoadPhase(opts: {
|
|
4
|
+
target: PerfTarget;
|
|
5
|
+
started: StartedTarget;
|
|
6
|
+
artifactsDir: string;
|
|
7
|
+
}): Promise<LoadMetrics>;
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/load/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAC9C,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAIxD,wBAAsB,YAAY,CAAC,IAAI,EAAE;IACvC,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,WAAW,CAAC,CA0DvB"}
|