Skip to content

Commit 19336b7

Browse files
Uzlopakanonrig
andauthored
perf: add fine grained benchmarks (#25)
* add benchmarks * adapt changes for fast-querystring * update package-lock.yaml * Apply suggestions from code review Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com> Co-authored-by: Yagiz Nizipli <yagiz@nizipli.com>
1 parent 9034bd3 commit 19336b7

5 files changed

Lines changed: 392 additions & 0 deletions

File tree

benchmark/bench-cmp-branch.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
"use strict";
2+
3+
const { spawn } = require("node:child_process");
4+
5+
const cliSelect = require("cli-select");
6+
const simpleGit = require("simple-git");
7+
8+
const git = simpleGit(process.cwd());
9+
10+
const COMMAND = "npm run benchmark";
11+
const DEFAULT_BRANCH = "main";
12+
const PERCENT_THRESHOLD = 5;
13+
const greyColor = "\x1b[30m";
14+
const redColor = "\x1b[31m";
15+
const greenColor = "\x1b[32m";
16+
const resetColor = "\x1b[0m";
17+
18+
async function selectBranchName(message, branches) {
19+
console.log(message);
20+
const result = await cliSelect({
21+
type: "list",
22+
name: "branch",
23+
values: branches,
24+
});
25+
console.log(result.value);
26+
return result.value;
27+
}
28+
29+
async function executeCommandOnBranch(command, branch) {
30+
console.log(`${greyColor}Checking out "${branch}"${resetColor}`);
31+
await git.checkout(branch);
32+
33+
console.log(`${greyColor}Execute "${command}"${resetColor}`);
34+
const childProcess = spawn(command, { stdio: "pipe", shell: true });
35+
36+
let result = "";
37+
childProcess.stdout.on("data", (data) => {
38+
process.stdout.write(data.toString());
39+
result += data.toString();
40+
});
41+
42+
await new Promise((resolve) => childProcess.on("close", resolve));
43+
44+
console.log();
45+
46+
return parseBenchmarksStdout(result);
47+
}
48+
49+
function parseBenchmarksStdout(text) {
50+
const results = [];
51+
52+
const lines = text.split("\n");
53+
for (const line of lines) {
54+
const match = /^(.+?)(\.*) x (.+) ops\/sec .*$/.exec(line);
55+
if (match !== null) {
56+
results.push({
57+
name: match[1],
58+
alignedName: match[1] + match[2],
59+
result: parseInt(match[3].split(",").join("")),
60+
});
61+
}
62+
}
63+
64+
return results;
65+
}
66+
67+
function compareResults(featureBranch, mainBranch) {
68+
for (const { name, alignedName, result: mainBranchResult } of mainBranch) {
69+
const featureBranchBenchmark = featureBranch.find(
70+
(result) => result.name === name,
71+
);
72+
if (featureBranchBenchmark) {
73+
const featureBranchResult = featureBranchBenchmark.result;
74+
const percent =
75+
((featureBranchResult - mainBranchResult) * 100) / mainBranchResult;
76+
const roundedPercent = Math.round(percent * 100) / 100;
77+
78+
const percentString =
79+
roundedPercent > 0 ? `+${roundedPercent}%` : `${roundedPercent}%`;
80+
const message = alignedName + percentString.padStart(7, ".");
81+
82+
if (roundedPercent > PERCENT_THRESHOLD) {
83+
console.log(`${greenColor}${message}${resetColor}`);
84+
} else if (roundedPercent < -PERCENT_THRESHOLD) {
85+
console.log(`${redColor}${message}${resetColor}`);
86+
} else {
87+
console.log(message);
88+
}
89+
}
90+
}
91+
}
92+
93+
(async function () {
94+
const branches = await git.branch();
95+
const currentBranch = branches.branches[branches.current];
96+
97+
let featureBranch = null;
98+
let mainBranch = null;
99+
100+
if (process.argv[2] === "--ci") {
101+
featureBranch = currentBranch.name;
102+
mainBranch = DEFAULT_BRANCH;
103+
} else {
104+
featureBranch = await selectBranchName(
105+
"Select the branch you want to compare (feature branch):",
106+
branches.all,
107+
);
108+
mainBranch = await selectBranchName(
109+
"Select the branch you want to compare with (main branch):",
110+
branches.all,
111+
);
112+
}
113+
114+
try {
115+
const featureBranchResult = await executeCommandOnBranch(
116+
COMMAND,
117+
featureBranch,
118+
);
119+
const mainBranchResult = await executeCommandOnBranch(COMMAND, mainBranch);
120+
compareResults(featureBranchResult, mainBranchResult);
121+
} catch (error) {
122+
console.error("Switch to origin branch due to an error", error.message);
123+
}
124+
125+
await git.checkout(currentBranch.commit);
126+
await git.checkout(currentBranch.name);
127+
128+
console.log(
129+
`${greyColor}Back to ${currentBranch.name} ${currentBranch.commit}${resetColor}`,
130+
);
131+
})();

benchmark/bench-thread.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"use strict";
2+
3+
const { workerData: benchmark, parentPort } = require("node:worker_threads");
4+
5+
const Benchmark = require("benchmark");
6+
Benchmark.options.minSamples = 100;
7+
8+
const suite = Benchmark.Suite();
9+
10+
const encodeString = require("../lib/internals/querystring").encodeString;
11+
const parse = require("../lib/parse");
12+
const stringify = require("../lib/stringify");
13+
14+
switch (benchmark.type) {
15+
case "encodeString":
16+
suite.add(`${benchmark.type}: ${benchmark.name}`, () => {
17+
encodeString(benchmark.input);
18+
});
19+
break;
20+
case "parse":
21+
suite.add(`${benchmark.type}: ${benchmark.name}`, () => {
22+
parse(benchmark.input);
23+
});
24+
break;
25+
case "stringify":
26+
suite.add(`${benchmark.type}: ${benchmark.name}`, () => {
27+
stringify(benchmark.input);
28+
});
29+
break;
30+
}
31+
32+
suite.on("cycle", (event) => {
33+
parentPort.postMessage(String(event.target));
34+
}).run();

benchmark/bench.js

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
"use strict";
2+
3+
const path = require("node:path");
4+
const { Worker } = require("node:worker_threads");
5+
6+
const BENCH_THREAD_PATH = path.join(__dirname, "bench-thread.js");
7+
8+
const benchmarks = [
9+
{
10+
type: "encodeString",
11+
name: '""',
12+
input: "",
13+
},
14+
{
15+
type: "encodeString",
16+
name: '"123"',
17+
input: "123",
18+
},
19+
{
20+
type: "encodeString",
21+
name: '"ä"',
22+
input: "ä",
23+
},
24+
{
25+
type: "encodeString",
26+
name: '"𝌆" ',
27+
input: "𝌆",
28+
},
29+
{
30+
type: "stringify",
31+
name: "undefined",
32+
input: undefined,
33+
},
34+
{
35+
type: "stringify",
36+
name: "null",
37+
input: null,
38+
},
39+
{
40+
type: "stringify",
41+
name: "{}",
42+
input: {},
43+
},
44+
{
45+
type: "stringify",
46+
name: "{ id: true }",
47+
input: { id: true },
48+
},
49+
{
50+
type: "stringify",
51+
name: "{ id: false }",
52+
input: { id: false },
53+
},
54+
{
55+
type: "stringify",
56+
name: "{ id: 123 }",
57+
input: { id: 123 },
58+
},
59+
{
60+
type: "stringify",
61+
name: "{ id: 1e+22 }",
62+
input: { id: 1e+22 },
63+
},
64+
{
65+
type: "stringify",
66+
name: "{ id: 123n }",
67+
input: { id: 123n },
68+
},
69+
{
70+
type: "stringify",
71+
name: "{ id: Infinity }",
72+
input: { id: Infinity },
73+
},
74+
{
75+
type: "stringify",
76+
name: '{ id: ["1", "3"] }',
77+
input: { id: ["1", "3"] },
78+
},
79+
{
80+
type: "stringify",
81+
name: '{ id: "" }',
82+
input: { id: "" },
83+
},
84+
{
85+
type: "stringify",
86+
name: '{ id: "123" }',
87+
input: { id: "123" },
88+
},
89+
{
90+
type: "stringify",
91+
name: '{ id: "ä" }',
92+
input: { id: "ä" },
93+
},
94+
{
95+
type: "stringify",
96+
name: '{ id: "𝌆" } ',
97+
input: { id: "𝌆" },
98+
},
99+
{
100+
type: "stringify",
101+
name: '{ foo: ["1", "3"], bar: "2" }',
102+
input: { foo: ["1", "3"], bar: "2" },
103+
},
104+
{
105+
type: "parse",
106+
name: "",
107+
input: "",
108+
},
109+
{
110+
type: "parse",
111+
name: "id=123",
112+
input: "id=123",
113+
},
114+
{
115+
type: "parse",
116+
name: "id=123&id=123",
117+
input: "id=123&id=123",
118+
},
119+
{
120+
type: "parse",
121+
name: "full%20name=Yagiz",
122+
input: "full%20name=Yagiz",
123+
},
124+
{
125+
type: "parse",
126+
name: "invalid%key=hello",
127+
input: "invalid%key=hello",
128+
},
129+
{
130+
type: "parse",
131+
name: "my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F",
132+
input: "my+weird+field=q1%212%22%27w%245%267%2Fz8%29%3F",
133+
},
134+
];
135+
136+
async function runBenchmark(benchmark) {
137+
const worker = new Worker(BENCH_THREAD_PATH, { workerData: benchmark });
138+
139+
return new Promise((resolve, reject) => {
140+
let result = null;
141+
worker.on("error", reject);
142+
worker.on("message", (benchResult) => {
143+
result = benchResult;
144+
});
145+
worker.on("exit", (code) => {
146+
if (code === 0) {
147+
resolve(result);
148+
} else {
149+
reject(new Error(`Worker stopped with exit code ${code}`));
150+
}
151+
});
152+
});
153+
}
154+
155+
async function runBenchmarks() {
156+
let maxNameLength = 0;
157+
for (const benchmark of benchmarks) {
158+
maxNameLength = Math.max(benchmark.name.length, maxNameLength);
159+
}
160+
161+
for (const benchmark of benchmarks) {
162+
benchmark.name = benchmark.name.padEnd(maxNameLength, " ");
163+
const resultMessage = await runBenchmark(benchmark);
164+
console.log(resultMessage);
165+
}
166+
}
167+
168+
runBenchmarks();

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"test:watch": "vitest --watch",
1313
"test:coverage": "vitest --coverage",
1414
"coverage": "vitest run --coverage",
15+
"benchmark": "node benchmark/bench.js",
16+
"benchmark:cmp-branch": "node benchmark/bench-cmp-branch.js",
1517
"benchmark:parse": "node benchmark/parse.mjs",
1618
"benchmark:stringify": "node benchmark/stringify.mjs",
1719
"benchmark:import": "node benchmark/import.mjs"
@@ -28,13 +30,16 @@
2830
"@aws-sdk/querystring-parser": "^3.162.0",
2931
"@types/node": "^18.7.15",
3032
"@vitest/coverage-c8": "^0.23.1",
33+
"benchmark": "^2.1.4",
34+
"cli-select": "^1.1.2",
3135
"cronometro": "^1.1.2",
3236
"http-querystring-stringify": "^2.1.0",
3337
"qs": "^6.11.0",
3438
"query-string": "^7.1.1",
3539
"querystringify": "^2.2.0",
3640
"querystringparser": "^0.1.1",
3741
"rome": "0.9.1-next",
42+
"simple-git": "^3.14.1",
3843
"vitest": "^0.23.1"
3944
},
4045
"repository": {

0 commit comments

Comments
 (0)