Skip to content

Commit e764ef6

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

File tree

16 files changed

+631
-1137
lines changed

16 files changed

+631
-1137
lines changed

package-lock.json

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

packages/autocannon/index.mjs

Lines changed: 3 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) => {
@@ -30,3 +32,4 @@ export default async function main (_opts = {}) {
3032

3133
return run(opts);
3234
}
35+

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: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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+
process = null;
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+
const proc = spawn(`${this.executable} ${shellQuote(args)}`, {
101+
shell: true
102+
}, opts);
103+
104+
let stderr = '';
105+
proc.stderr.setEncoding('utf8');
106+
proc.stderr.on('data', (chunk) => {
107+
stderr += chunk
108+
});
109+
110+
let stdout = '';
111+
proc.stdout.setEncoding('utf8');
112+
proc.stdout.on('data', (chunk) => {
113+
stdout += chunk
114+
});
115+
116+
const onError = (e, data) => {
117+
Object.assign(e, { stderr, stdout, ...data });
118+
this.status = e;
119+
reject(e);
120+
}
121+
122+
proc.once('error', (e) => onError(e));
123+
124+
proc.once('close', (code) => {
125+
if (code) {
126+
return onError(new Error(`${this.executable} failed with ${code}`, { code }));
127+
}
128+
resolve(stdout);
129+
});
130+
131+
});
132+
}
133+
}

packages/cli/load.mjs

Lines changed: 11 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,7 @@ 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(...results);
142148
}
143149
completed = true;
144150

packages/requests/index.mjs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { AutocannonCLI } from '@expressjs/perf-autocannon';
2+
import { Wrk2CLI } from '@expressjs/perf-wrk2';
3+
4+
export function startLoad (opts = {}) {
5+
const requests = opts.requests || [{
6+
method: 'GET',
7+
path: '/',
8+
headers: {},
9+
body: undefined
10+
}];
11+
const requesters = [];
12+
13+
if (opts.useAutocannon !== false) {
14+
const ac = new AutocannonCLI({
15+
duration: opts.duration,
16+
connections: opts.connections,
17+
method: opts.method,
18+
url: opts.url,
19+
headers: opts.headers,
20+
body: opts.body
21+
});
22+
requesters.push(ac);
23+
}
24+
25+
if (opts.useWrk2 !== false) {
26+
const wrk = new Wrk2CLI({
27+
duration: opts.duration,
28+
connections: opts.connections,
29+
rate: opts.rate,
30+
method: opts.method,
31+
url: opts.url,
32+
headers: opts.headers,
33+
body: opts.body
34+
});
35+
requesters.push(wrk);
36+
}
37+
38+
const toAwait = requesters.flatMap((requester) => {
39+
return requests.map((request) => {
40+
return requester.start(request);
41+
});
42+
})
43+
44+
return {
45+
close: async () => {
46+
// TODO: need to implement this in the cli wrappers
47+
},
48+
results: () => {
49+
return Promise.allSettled(toAwait);
50+
}
51+
};
52+
53+
}

packages/requests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"name": "@expressjs/perf-requests",
33
"version": "1.0.0",
44
"exports": {
5+
".": "./index.mjs",
56
"./get-basic-paths": "./get-basic-paths.mjs",
67
"./get-slash": "./get-slash.mjs",
78
"./get-query": "./get-query.mjs",

packages/runner-local-docker/index.mjs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -146,20 +146,20 @@ export function createRunner (runnerConfig = {}) {
146146
const opts = { ..._opts };
147147
if (opts?.signal.aborted) return;
148148

149-
const cannon = ac({
150-
url: server.metadata.url.toString(),
149+
const load = startLoad({
150+
url: server.metadata.url,
151151
requests: await (await import(opts.test)).requests(),
152152
duration: opts.duration
153153
});
154154

155155
opts?.signal.addEventListener('abort', () => {
156-
cannon.stop?.();
156+
load.close();
157157
});
158158

159159
return {
160160
metadata: collectMetadata(),
161-
close: async () => cannon.stop?.(),
162-
results: () => cannon
161+
close: () => load.close(),
162+
results: () => load.results()
163163
};
164164
}
165165

packages/runner-local-docker/scripts/start.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ node metadata.mjs > $RESULTS_DIR/metadata.json
1818
# If not, require a repo and ref
1919
REPO_DIR="./repo"
2020
if [ -d "$REPO_DIR" ]; then
21-
echo "Using local benchmarks"
21+
echo "Using local repo."
2222
cd "$REPO_DIR"
2323
else
2424
if [ -z "$REPO" ]; then

packages/runner-local/client.mjs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { startLoad } from '@expressjs/perf-requests';
2+
import { collectMetadata } from '@expressjs/perf-metadata';
3+
4+
export async function startClient (opts = {}) {
5+
if (opts.signal.aborted) {
6+
return;
7+
}
8+
9+
const load = startLoad(opts);
10+
11+
return {
12+
metadata: collectMetadata(),
13+
close: async () => {
14+
// TODO: need to implement this
15+
// load.close();
16+
},
17+
results: () => {
18+
return load.results();
19+
}
20+
};
21+
}

0 commit comments

Comments
 (0)