Skip to content

Commit b5706bc

Browse files
committed
WIP: convert to local runner
1 parent 8787274 commit b5706bc

File tree

10 files changed

+411
-1135
lines changed

10 files changed

+411
-1135
lines changed

package-lock.json

Lines changed: 15 additions & 1130 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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { spawnSync, spawn } from 'node:child_process';
12
import autocannon from 'autocannon';
23

34
export function run (opts) {
@@ -30,3 +31,51 @@ export default async function main (_opts = {}) {
3031

3132
return run(opts);
3233
}
34+
35+
export class AutocannonBenchmarker {
36+
constructor() {
37+
const shell = (process.platform === 'win32');
38+
this.name = 'autocannon';
39+
this.opts = { shell };
40+
this.executable = shell ? 'autocannon.cmd' : 'autocannon';
41+
const result = spawnSync(this.executable, ['-h'], this.opts);
42+
if (shell) {
43+
this.present = (result.status === 0);
44+
} else {
45+
this.present = !(result.error && result.error.code === 'ENOENT');
46+
}
47+
}
48+
49+
create(options) {
50+
const args = [
51+
'-d', options.duration,
52+
'-c', options.connections,
53+
'-j',
54+
'-n',
55+
];
56+
for (const field in options.headers) {
57+
if (this.opts.shell) {
58+
args.push('-H', `'${field}=${options.headers[field]}'`);
59+
} else {
60+
args.push('-H', `${field}=${options.headers[field]}`);
61+
}
62+
}
63+
const scheme = options.scheme || 'http';
64+
args.push(`${scheme}://127.0.0.1:${options.port}${options.path}`);
65+
const child = spawn(this.executable, args, this.opts);
66+
return child;
67+
}
68+
69+
processResults(output) {
70+
let result;
71+
try {
72+
result = JSON.parse(output);
73+
} catch {
74+
return undefined;
75+
}
76+
if (!result || !result.requests || !result.requests.average) {
77+
return undefined;
78+
}
79+
return result.requests.average;
80+
}
81+
}

packages/cli/load.mjs

Lines changed: 9 additions & 4 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
@@ -53,9 +53,9 @@ export default function main (_opts = {}) {
5353

5454
const opts = {
5555
cwd,
56-
repo: 'https://github.com/expressjs/perf-wg.git',
57-
repoRef: 'master',
58-
runner: '@expressjs/perf-runner-vanilla',
56+
repo: null, // 'https://github.com/expressjs/perf-wg.git',
57+
repoRef: null, // 'master',
58+
runner: '@expressjs/perf-runner-local',
5959
test: '@expressjs/perf-load-example',
6060
node: 'lts_latest',
6161
...conf,
@@ -109,6 +109,11 @@ export default function main (_opts = {}) {
109109
const vers = await nv(opts.node, {
110110
latestOfMajorOnly: true
111111
});
112+
if (!vers?.[0]?.version) {
113+
throw Object.assign(new Error(`Unable to resolve node version`), {
114+
spec: opts.node
115+
});
116+
}
112117

113118
const results = await runner({
114119
cwd: opts.cwd,

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

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

packages/runner-local/index.mjs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { dirname } from 'node:path';
2+
import { fileURLToPath } from 'node:url';
3+
import { styleText } from 'node:util';
4+
import { AutocannonBenchmarker } from '@expressjs/perf-autocannon';
5+
import { Wrk2Benchmarker } from '@expressjs/perf-wrk2';
6+
7+
import { setup, cleanup } from './setup.mjs';
8+
import { startServer } from './server.mjs';
9+
10+
function runBenchmarker(instance, options) {
11+
console.log(styleText(['blue'], `Running ${instance.name} load...`));
12+
return new Promise((resolve, reject) => {
13+
const proc = instance.create(options);
14+
proc.stderr.pipe(process.stderr);
15+
16+
let stdout = '';
17+
proc.stdout.setEncoding('utf8');
18+
proc.stdout.on('data', (chunk) => stdout += chunk);
19+
20+
proc.once('close', (code) => {
21+
if (code) {
22+
let error_message = `${instance.name} failed with ${code}.`;
23+
if (stdout !== '') {
24+
error_message += ` Output: ${stdout}`;
25+
}
26+
reject(new Error(error_message), code);
27+
return;
28+
}
29+
30+
const result = instance.processResults(stdout);
31+
resolve(result);
32+
});
33+
})
34+
}
35+
36+
export async function startClient (_opts = {}, server) {
37+
const opts = {
38+
..._opts
39+
};
40+
if (opts?.signal.aborted) {
41+
return;
42+
}
43+
44+
const autocannon = new AutocannonBenchmarker();
45+
const wrk2 = new Wrk2Benchmarker();
46+
const waitFor = [];
47+
48+
if (!wrk2.present) {
49+
console.log(styleText(['bold', 'yellow'], 'Wrk2 not found. Please install it'));
50+
} else {
51+
waitFor.push(
52+
runBenchmarker(wrk2, {
53+
duration: 30,
54+
connections: 100,
55+
rate: 2000,
56+
port: 3000,
57+
path: '/'
58+
})
59+
);
60+
console.log('Running Wrk2');
61+
}
62+
63+
if (!autocannon.present) {
64+
console.log(styleText(['bold', 'yellow'], 'Autocannon not found. Please install it with `npm i -g autocannon`'));
65+
} else {
66+
waitFor.push(
67+
runBenchmarker(autocannon, {
68+
duration: 30,
69+
connections: 100,
70+
port: 3000,
71+
path: '/',
72+
})
73+
);
74+
console.log('Running Autocannon');
75+
}
76+
77+
78+
return {
79+
metadata: {},
80+
close: async () => {
81+
// TODO: need to get the client classes into order for this
82+
},
83+
results: () => {
84+
return Promise.allSettled(waitFor);
85+
}
86+
};
87+
}
88+
89+
export default async function runner (_opts = {}) {
90+
// Start up the server, then the client
91+
const opts = {
92+
..._opts
93+
};
94+
95+
const cwd = fileURLToPath(dirname(import.meta.resolve(opts.test)));
96+
await setup(cwd, opts);
97+
98+
const server = await startServer(cwd, opts);
99+
// const client = await startClient(opts, server);
100+
101+
// Wait for the client to finish, then the server
102+
// const clientResults = await client.results();
103+
const serverResults = await server.results();
104+
105+
// await client.close();
106+
await server.close();
107+
108+
await cleanup(cwd);
109+
110+
return {
111+
serverMetadata: server.metadata,
112+
// clientMetadata: client.metadata,
113+
// serverResults,
114+
// clientResults
115+
};
116+
}

packages/runner-local/package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "@expressjs/perf-runner-local",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.mjs",
6+
"devDependencies": {},
7+
"scripts": {
8+
"test": "echo \"Error: no test specified\" && exit 1"
9+
},
10+
"keywords": [],
11+
"author": "",
12+
"license": "ISC"
13+
}

packages/runner-local/server.mjs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { execFile } from 'node:child_process';
2+
import { fileURLToPath } from 'node:url';
3+
4+
export async function startServer (cwd, opts = {}) {
5+
return new Promise((resolve, reject) => {
6+
const test = fileURLToPath(import.meta.resolve(opts.test));
7+
const cp = execFile(process.execPath, [ test ], { cwd });
8+
9+
const server = {
10+
metadata: {
11+
url: new URL('http://localhost:3000'),
12+
},
13+
status: 'starting',
14+
close: () => {
15+
return new Promise((closeResolve) => {
16+
cp.on('exit', () => {
17+
closeResolve();
18+
});
19+
cp.kill('SIGINT');
20+
});
21+
},
22+
results: async () => {
23+
return { };
24+
}
25+
};
26+
27+
opts?.signal.addEventListener('abort', () => {
28+
server.status = 'aborted';
29+
cp.kill('SIGINT');
30+
reject(new Error('aborted'));
31+
});
32+
cp.on('error', reject);
33+
cp.stdout.on('data', (d) => {
34+
process.stdout.write(d);
35+
if (server.status === 'starting' && d.toString('utf8').includes("startup:")) {
36+
server.status = 'started';
37+
resolve(server);
38+
}
39+
});
40+
cp.stderr.on('data', (d) => {
41+
process.stderr.write(d);
42+
});
43+
});
44+
}

packages/runner-local/setup.mjs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {
2+
rename,
3+
access,
4+
writeFile,
5+
rm,
6+
realpath,
7+
constants as fsConstants
8+
} from 'node:fs/promises';
9+
import { dirname, join } from 'node:path';
10+
import { promisify } from 'node:util';
11+
import { execFile } from 'node:child_process';
12+
13+
const pExecFile = promisify(execFile);
14+
15+
async function restore (cwd) {
16+
const pkgPath = join(cwd, 'package.json');
17+
const pkgBakPath = join(cwd, 'package.json.bak');
18+
try {
19+
await access(pkgBakPath, fsConstants.R_OK | fsConstants.W_OK);
20+
console.log('package.json.bak exists, restoring original');
21+
await rm(pkgPath);
22+
await rename(pkgBakPath, pkgPath, fsConstants.COPYFILE_FICLONE);
23+
} catch (e) {
24+
if (e.code !== 'ENOENT') {
25+
throw e;
26+
}
27+
}
28+
return { pkgPath, pkgBakPath };
29+
}
30+
31+
export async function setup (cwd, opts = {}) {
32+
// Ensure we have the right node version
33+
if (process.version !== `v${opts.node}`) {
34+
// TODO: install node with nvm?
35+
throw Object.assign(new Error(`Incorrect node.js version`), {
36+
wanted: opts.node,
37+
found: process.version.replace(/^v/, '')
38+
});
39+
}
40+
41+
// Setup the repo if asked
42+
if (!opts.repo) {
43+
console.log('Using local repo.');
44+
} else {
45+
// TODO: clone repo into tmp dir
46+
throw new Error('cloning repo not yet implemented');
47+
}
48+
49+
// Restore backup if necessary
50+
const { pkgPath, pkgBakPath } = await restore(cwd);
51+
52+
// Read in package.json contets
53+
const pkg = await import(pkgPath, {
54+
with: {
55+
type: 'json'
56+
}
57+
});
58+
59+
// Apply overrides
60+
if (opts.overrides) {
61+
await rename(pkgPath, pkgBakPath);
62+
pkg.overrides = {
63+
...(pkg.overrides ?? {}),
64+
...opts.overrides
65+
};
66+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2));
67+
}
68+
69+
// node --run setup || npm run setup
70+
try {
71+
const nodePath = await realpath(process.execPath);
72+
await pExecFile(nodePath, ['--run', 'setup'], { cwd });
73+
} catch (e) {
74+
const npmPath = await realpath(join(dirname(process.execPath), 'npm'));
75+
await pExecFile(npmPath, ['run', 'setup'], { cwd });
76+
}
77+
78+
return { cwd };
79+
}
80+
81+
export async function cleanup (cwd) {
82+
await restore(cwd);
83+
await rm(join(cwd, 'package-lock.json'), {
84+
force: true
85+
});
86+
await rm(join(cwd, 'node_modules'), {
87+
force: true,
88+
recursive: true
89+
});
90+
}

0 commit comments

Comments
 (0)