Skip to content

Commit 2f77288

Browse files
Run benchmarks in separate threads (#418)
1 parent a0aed60 commit 2f77288

File tree

5 files changed

+350
-2
lines changed

5 files changed

+350
-2
lines changed

benchmark/bench-cmp-branch.js

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

bench.js renamed to benchmark/bench-cmp-lib.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ const CJSStringifyArray = CJS(arraySchemaCJS)
102102
const CJSStringifyDate = CJS(dateFormatSchemaCJS)
103103
const CJSStringifyString = CJS({ type: 'string' })
104104

105-
const FJS = require('.')
105+
const FJS = require('..')
106106
const stringify = FJS(schema)
107107
const stringifyArrayDefault = FJS(arraySchema)
108108
const stringifyArrayJSONStringify = FJS(arraySchema, {

benchmark/bench-thread.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
'use strict'
2+
3+
const { workerData: benchmark, parentPort } = require('worker_threads')
4+
5+
const Benchmark = require('benchmark')
6+
Benchmark.options.minSamples = 500
7+
8+
const suite = Benchmark.Suite()
9+
10+
const FJS = require('..')
11+
const stringify = FJS(benchmark.schema)
12+
13+
suite
14+
.add(benchmark.name, () => {
15+
stringify(benchmark.input)
16+
})
17+
.on('cycle', (event) => {
18+
parentPort.postMessage(String(event.target))
19+
})
20+
.on('complete', () => {})
21+
.run()

benchmark/bench.js

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
'use strict'
2+
3+
const path = require('path')
4+
const { Worker } = require('worker_threads')
5+
6+
const BENCH_THREAD_PATH = path.join(__dirname, 'bench-thread.js')
7+
8+
const LONG_STRING_LENGTH = 1e4
9+
const SHORT_ARRAY_SIZE = 1e3
10+
11+
const shortArrayOfNumbers = new Array(SHORT_ARRAY_SIZE)
12+
const shortArrayOfIntegers = new Array(SHORT_ARRAY_SIZE)
13+
const shortArrayOfShortStrings = new Array(SHORT_ARRAY_SIZE)
14+
const shortArrayOfLongStrings = new Array(SHORT_ARRAY_SIZE)
15+
const shortArrayOfMultiObject = new Array(SHORT_ARRAY_SIZE)
16+
17+
function getRandomInt (max) {
18+
return Math.floor(Math.random() * max)
19+
}
20+
21+
let longString = ''
22+
for (let i = 0; i < LONG_STRING_LENGTH; i++) {
23+
longString += i
24+
if (i % 100 === 0) {
25+
longString += '"'
26+
}
27+
}
28+
29+
for (let i = 0; i < SHORT_ARRAY_SIZE; i++) {
30+
shortArrayOfNumbers[i] = getRandomInt(1000)
31+
shortArrayOfIntegers[i] = getRandomInt(1000)
32+
shortArrayOfShortStrings[i] = 'hello world'
33+
shortArrayOfLongStrings[i] = longString
34+
shortArrayOfMultiObject[i] = { s: 'hello world', n: 42, b: true }
35+
}
36+
37+
const benchmarks = [
38+
{
39+
name: 'short string',
40+
schema: {
41+
type: 'string'
42+
},
43+
input: 'hello world'
44+
},
45+
{
46+
name: 'long string',
47+
schema: {
48+
type: 'string'
49+
},
50+
input: longString
51+
},
52+
{
53+
name: 'number',
54+
schema: {
55+
type: 'number'
56+
},
57+
input: 42
58+
},
59+
{
60+
name: 'integer',
61+
schema: {
62+
type: 'integer'
63+
},
64+
input: 42
65+
},
66+
{
67+
name: 'formatted date',
68+
schema: {
69+
type: 'string',
70+
format: 'date'
71+
},
72+
input: new Date()
73+
},
74+
{
75+
name: 'short array of numbers',
76+
schema: {
77+
type: 'array',
78+
items: { type: 'number' }
79+
},
80+
input: shortArrayOfNumbers
81+
},
82+
{
83+
name: 'short array of integers',
84+
schema: {
85+
type: 'array',
86+
items: { type: 'integer' }
87+
},
88+
input: shortArrayOfIntegers
89+
},
90+
{
91+
name: 'short array of short strings',
92+
schema: {
93+
type: 'array',
94+
items: { type: 'string' }
95+
},
96+
input: shortArrayOfShortStrings
97+
},
98+
{
99+
name: 'short array of long strings',
100+
schema: {
101+
type: 'array',
102+
items: { type: 'string' }
103+
},
104+
input: shortArrayOfShortStrings
105+
},
106+
{
107+
name: 'short array of objects with properties of different types',
108+
schema: {
109+
type: 'array',
110+
items: {
111+
type: 'object',
112+
properties: {
113+
s: { type: 'string' },
114+
n: { type: 'number' },
115+
b: { type: 'boolean' }
116+
}
117+
}
118+
},
119+
input: shortArrayOfMultiObject
120+
},
121+
{
122+
name: 'object with number property',
123+
schema: {
124+
type: 'object',
125+
properties: {
126+
a: { type: 'number' }
127+
}
128+
},
129+
input: { a: 42 }
130+
},
131+
{
132+
name: 'object with integer property',
133+
schema: {
134+
type: 'object',
135+
properties: {
136+
a: { type: 'integer' }
137+
}
138+
},
139+
input: { a: 42 }
140+
},
141+
{
142+
name: 'object with short string property',
143+
schema: {
144+
type: 'object',
145+
properties: {
146+
a: { type: 'string' }
147+
}
148+
},
149+
input: { a: 'hello world' }
150+
},
151+
{
152+
name: 'object with long string property',
153+
schema: {
154+
type: 'object',
155+
properties: {
156+
a: { type: 'string' }
157+
}
158+
},
159+
input: { a: longString }
160+
},
161+
{
162+
name: 'object with properties of different types',
163+
schema: {
164+
type: 'object',
165+
properties: {
166+
s: { type: 'string' },
167+
n: { type: 'number' },
168+
b: { type: 'boolean' }
169+
}
170+
},
171+
input: { s: 'hello world', n: 42, b: true }
172+
}
173+
]
174+
175+
async function runBenchmark (benchmark) {
176+
const worker = new Worker(BENCH_THREAD_PATH, { workerData: benchmark })
177+
178+
return new Promise((resolve, reject) => {
179+
let result = null
180+
worker.on('error', reject)
181+
worker.on('message', (benchResult) => {
182+
result = benchResult
183+
})
184+
worker.on('exit', (code) => {
185+
if (code === 0) {
186+
resolve(result)
187+
} else {
188+
reject(new Error(`Worker stopped with exit code ${code}`))
189+
}
190+
})
191+
})
192+
}
193+
194+
async function runBenchmarks () {
195+
let maxNameLength = 0
196+
for (const benchmark of benchmarks) {
197+
maxNameLength = Math.max(benchmark.name.length, maxNameLength)
198+
}
199+
200+
for (const benchmark of benchmarks) {
201+
benchmark.name = benchmark.name.padEnd(maxNameLength, '.')
202+
const resultMessage = await runBenchmark(benchmark)
203+
console.log(resultMessage)
204+
}
205+
}
206+
207+
runBenchmarks()

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
"main": "index.js",
66
"types": "index.d.ts",
77
"scripts": {
8-
"benchmark": "node bench.js",
8+
"bench": "node ./benchmark/bench.js",
9+
"bench:cmp": "node ./benchmark/bench-cmp-branch.js",
10+
"bench:cmp:ci": "node ./benchmark/bench-cmp-branch.js --ci",
11+
"benchmark": "node ./benchmark/bench-cmp-lib.js",
912
"lint:fix": "standard --fix",
1013
"test:lint": "standard",
1114
"test:typescript": "tsc --project ./test/types/tsconfig.json",
@@ -32,12 +35,15 @@
3235
"devDependencies": {
3336
"@sinclair/typebox": "^0.23.3",
3437
"benchmark": "^2.1.4",
38+
"chalk": "^4.1.2",
3539
"compile-json-stringify": "^0.1.2",
40+
"inquirer": "^8.2.4",
3641
"is-my-json-valid": "^2.20.0",
3742
"moment": "^2.24.0",
3843
"pre-commit": "^1.2.2",
3944
"proxyquire": "^2.1.3",
4045
"semver": "^7.1.0",
46+
"simple-git": "^3.7.1",
4147
"standard": "^17.0.0",
4248
"tap": "^16.0.1",
4349
"typescript": "^4.0.2",

0 commit comments

Comments
 (0)