Skip to content

Commit a61605e

Browse files
committed
WIP: convert to local runner
1 parent 63c0faf commit a61605e

File tree

22 files changed

+677
-1375
lines changed

22 files changed

+677
-1375
lines changed

package-lock.json

Lines changed: 39 additions & 1126 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
"description": "ExpressJS Performance Working Group",
55
"scripts": {
66
"setup": "npm i",
7-
"local-server": "expf local-server",
8-
"local-load": "expf local-load",
97
"test": "echo \"Error: no test specified\" && exit 1",
108
"lint": "npm run --workspaces --if-present lint",
119
"lint:fix": "npm run --workspaces --if-present lint:fix",

packages/autocannon/index.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import autocannon from 'autocannon';
22

3+
export * from './spawn.mjs';
4+
35
export function run (opts) {
46
return new Promise((resolve, reject) => {
57
autocannon(opts, (err, result) => {

packages/autocannon/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"license": "ISC",
1313
"description": "",
1414
"dependencies": {
15-
"autocannon": "^8.0.0"
15+
"autocannon": "^8.0.0",
16+
"shell-quote": "^1.8.3"
1617
},
1718
"devDependencies": {
1819
"semistandard": "^17.0.0"

packages/autocannon/spawn.mjs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { availableParallelism } from 'node:os';
2+
import { spawn } from 'node:child_process';
3+
import { quote as shellQuote } from 'shell-quote';
4+
5+
export class AutocannonCLI {
6+
// NOTE: I don't use windows or have a windows machine to test on, so this
7+
// was just carried over from the previous implementation, someone
8+
// should go and test I didn't break this
9+
executable = process.platform === 'win32' ? 'autocannon.cmd' : 'autocannon';
10+
isPresent = null;
11+
status = null;
12+
procs = [];
13+
14+
duration = 60;
15+
connections = 100;
16+
method = 'GET';
17+
url = new URL('http://127.0.0.1:3000/');
18+
headers = {};
19+
body;
20+
21+
constructor (opts = {}) {
22+
// autocannon options
23+
this.duration = opts.duration || this.duration;
24+
this.connections = opts.connections || this.connections;
25+
26+
// request options
27+
this.method = opts.method || this.method;
28+
this.url = opts.url || this.url;
29+
this.headers = opts.headers || this.headers;
30+
this.body = opts.body || this.body;
31+
}
32+
33+
async start (opts = {}) {
34+
// Check if present, but only once
35+
if (this.isPresent === null) {
36+
try {
37+
await this.spawn(['-h']);
38+
this.isPresent = true;
39+
} catch (e) {
40+
// Set the status to the error we got
41+
this.status = e;
42+
this.isPresent = false;
43+
}
44+
}
45+
46+
if (!this.isPresent) {
47+
this.status = this.status || 'not present';
48+
throw new Error('executable not present', {
49+
cause: this.status
50+
});
51+
}
52+
53+
this.status = 'starting';
54+
55+
const args = [
56+
// -d/--duration SEC
57+
'-d', this.duration,
58+
// -c/--connections NUM
59+
'-c', this.connections,
60+
// -w/--workers NUM
61+
'-w', Math.min(this.connections, availableParallelism() || 8),
62+
63+
// -j/--json
64+
'-j',
65+
// -n/--no-progress
66+
'-n',
67+
68+
// -m/--method METHOD
69+
'-m', opts.method || this.method
70+
];
71+
72+
// -b/--body BODY
73+
const body = opts.body || this.body;
74+
if (body) {
75+
args.push('-b', typeof body === 'string' ? body : JSON.stringify(body));
76+
}
77+
78+
// -H/--headers K=V
79+
for (const [header, value] of Object.entries(opts.headers || this.headers)) {
80+
args.push('-H', `${header}=${value}`);
81+
}
82+
83+
// url (positional)
84+
const url = opts.url ? new URL(opts.url, this.url) : this.url;
85+
args.push(url.toString());
86+
87+
// Run the process
88+
const stdout = await this.spawn(args);
89+
90+
const result = JSON.parse(stdout);
91+
if (!result.requests.average) {
92+
throw new Error(`${this.executable} produced invalid JSON output`);
93+
}
94+
95+
return result;
96+
}
97+
98+
async spawn (args = [], opts = {}) {
99+
return new Promise((resolve, reject) => {
100+
if (this.status === 'aborted') {
101+
return reject(new Error('Aborted before start'));
102+
}
103+
const proc = spawn(`${this.executable} ${shellQuote(args)}`, {
104+
shell: true
105+
}, opts);
106+
107+
this.procs.push(proc);
108+
109+
let stderr = '';
110+
proc.stderr.setEncoding('utf8');
111+
proc.stderr.on('data', (chunk) => {
112+
stderr += chunk;
113+
});
114+
115+
let stdout = '';
116+
proc.stdout.setEncoding('utf8');
117+
proc.stdout.on('data', (chunk) => {
118+
stdout += chunk;
119+
});
120+
121+
const onError = (e, data) => {
122+
Object.assign(e, { stderr, stdout, ...data });
123+
this.status = e;
124+
reject(e);
125+
};
126+
127+
proc.once('error', (e) => onError(e));
128+
129+
proc.once('close', (code) => {
130+
if (code) {
131+
return onError(new Error(`${this.executable} failed with ${code}`, { code }));
132+
}
133+
resolve(stdout);
134+
});
135+
});
136+
}
137+
138+
close () {
139+
this.procs.forEach((proc) => {
140+
proc.kill('SIGINT');
141+
});
142+
this.status = 'aborted';
143+
}
144+
}

packages/cli/bin/expf.mjs

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,20 +88,6 @@ switch (positionals[2]) {
8888
console.error(e);
8989
}
9090
break;
91-
case 'local-server':
92-
try {
93-
await (await import('../local-server.mjs')).default(values);
94-
} catch (e) {
95-
console.error(e);
96-
}
97-
break;
98-
case 'local-load':
99-
try {
100-
await (await import('../local-load.mjs')).default(values);
101-
} catch (e) {
102-
console.error(e);
103-
}
104-
break;
10591
default:
10692
console.log(`
10793
Express Performance Testing CLI

packages/cli/load.mjs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export function help (opts = {}) {
1111
Flags:
1212
1313
--cwd=${opts.cwd || normalize(join(import.meta.dirname, '..', '..'))}
14-
--runner=@expressjs/perf-runner-vanilla
14+
--runner=@expressjs/perf-runner-local
1515
--repo=https://github.com/expressjs/perf-wg.git
1616
--repo-ref=master
1717
--test=@expressjs/perf-load-example
@@ -59,9 +59,9 @@ export default function main (_opts = {}) {
5959

6060
const opts = {
6161
cwd,
62-
repo: 'https://github.com/expressjs/perf-wg.git',
63-
repoRef: 'master',
64-
runner: '@expressjs/perf-runner-vanilla',
62+
repo: null, // 'https://github.com/expressjs/perf-wg.git',
63+
repoRef: null, // 'master',
64+
runner: '@expressjs/perf-runner-local',
6565
test: '@expressjs/perf-load-example',
6666
node: 'lts_latest',
6767
...conf,
@@ -116,6 +116,12 @@ export default function main (_opts = {}) {
116116
vers = await nv(opts.node, {
117117
latestOfMajorOnly: true
118118
});
119+
120+
if (!vers?.[0]?.version) {
121+
throw Object.assign(new Error(`Unable to resolve node version`), {
122+
spec: opts.node
123+
});
124+
}
119125
} catch (e) {
120126
// If offline or cannt load node versions, use
121127
// the option as passed in without a default
@@ -138,7 +144,9 @@ export default function main (_opts = {}) {
138144
await writeFile(outputFile, JSON.stringify(results, null, 2));
139145
console.log(`written to: ${outputFile}`);
140146
} else {
141-
console.log(results);
147+
console.log(...Object.entries(results).flatMap(([k, v]) => {
148+
return ['\n', k, '\n', v];
149+
}));
142150
}
143151
completed = true;
144152

0 commit comments

Comments
 (0)