Skip to content

Commit 82ab642

Browse files
hoeckmoltar
authored andcommitted
benchmark each module in their own node process
1 parent 374e311 commit 82ab642

File tree

8 files changed

+240
-47
lines changed

8 files changed

+240
-47
lines changed

README.md

+7
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,10 @@ function isMyDataValid(data: any) {
7474
// `res` is now type casted to the right type
7575
const res = isMyDataValid(data)
7676
```
77+
78+
## Local Development
79+
80+
* `npm run start` - run benchmarks for all modules
81+
* `npm run start run zod myzod valita` - run benchmarks only for a few selected modules
82+
* `npm run docs:serve` - result viewer
83+
* `npm run test` - run tests on all modules

benchmarks/helpers/main.ts

+77-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { add, complete, cycle, suite } from 'benny';
2-
import { readFileSync, writeFileSync } from 'fs';
2+
import { readFileSync, writeFileSync, existsSync, unlinkSync } from 'fs';
33
import { join } from 'path';
44
import { writePreviewGraph } from './graph';
55
import { getRegisteredBenchmarks } from './register';
@@ -10,15 +10,18 @@ const NODE_VERSION = process.env.NODE_VERSION || process.version;
1010
const NODE_VERSION_FOR_PREVIEW = 17;
1111
const TEST_PREVIEW_GENERATION = false;
1212

13-
export async function main() {
13+
/**
14+
* Run all registered benchmarks and append the results to a file.
15+
*/
16+
export async function runAllBenchmarks() {
1417
if (TEST_PREVIEW_GENERATION) {
15-
// just generate the preview without using benchmark data from a previous run
18+
// during development: generate the preview using benchmark data from a previous run
1619
const allResults: BenchmarkResult[] = JSON.parse(
1720
readFileSync(join(DOCS_DIR, 'results', 'node-17.json')).toString()
1821
).results;
1922

2023
await writePreviewGraph({
21-
filename: join(DOCS_DIR, 'results', 'preview.svg'),
24+
filename: previewSvgFilename(),
2225
values: allResults,
2326
});
2427

@@ -42,24 +45,40 @@ export async function main() {
4245
});
4346
}
4447

45-
writeFileSync(
46-
join(DOCS_DIR, 'results', `node-${majorVersion}.json`),
48+
// collect results of isolated benchmark runs into a single file
49+
appendResults(allResults);
50+
}
4751

48-
JSON.stringify({
49-
results: allResults,
50-
}),
52+
/**
53+
* Remove the results json file.
54+
*/
55+
export function deleteResults() {
56+
const fileName = resultsJsonFilename();
5157

52-
{ encoding: 'utf8' }
53-
);
58+
if (existsSync(fileName)) {
59+
unlinkSync(fileName);
60+
}
61+
}
62+
63+
/**
64+
* Generate the preview svg shown in the readme.
65+
*/
66+
export async function createPreviewGraph() {
67+
const majorVersion = getNodeMajorVersion();
5468

5569
if (majorVersion === NODE_VERSION_FOR_PREVIEW) {
70+
const allResults: BenchmarkResult[] = JSON.parse(
71+
readFileSync(resultsJsonFilename()).toString()
72+
).results;
73+
5674
await writePreviewGraph({
57-
filename: join(DOCS_DIR, 'results', 'preview.svg'),
75+
filename: previewSvgFilename(),
5876
values: allResults,
5977
});
6078
}
6179
}
6280

81+
// run a benchmark fn with benny
6382
async function runBenchmarks(name: string, cases: BenchmarkCase[]) {
6483
const fns = cases.map(c => add(c.moduleName, () => c.run()));
6584

@@ -74,6 +93,52 @@ async function runBenchmarks(name: string, cases: BenchmarkCase[]) {
7493
);
7594
}
7695

96+
// append results to an existing file or create a new one
97+
function appendResults(results: BenchmarkResult[]) {
98+
const fileName = resultsJsonFilename();
99+
const existingResults: BenchmarkResult[] = existsSync(fileName)
100+
? JSON.parse(readFileSync(fileName).toString()).results
101+
: [];
102+
103+
// check that we're appending unique data
104+
const getKey = ({
105+
benchmark,
106+
name,
107+
nodeVersion,
108+
}: BenchmarkResult): string => {
109+
return JSON.stringify({ benchmark, name, nodeVersion });
110+
};
111+
const existingResultsIndex = new Set(existingResults.map(r => getKey(r)));
112+
113+
results.forEach(r => {
114+
if (existingResultsIndex.has(getKey(r))) {
115+
console.error('Result %s already exists in', getKey(r), fileName);
116+
117+
throw new Error('Duplicate result in result json file');
118+
}
119+
});
120+
121+
writeFileSync(
122+
fileName,
123+
124+
JSON.stringify({
125+
results: [...existingResults, ...results],
126+
}),
127+
128+
{ encoding: 'utf8' }
129+
);
130+
}
131+
132+
function resultsJsonFilename() {
133+
const majorVersion = getNodeMajorVersion();
134+
135+
return join(DOCS_DIR, 'results', `node-${majorVersion}.json`);
136+
}
137+
138+
function previewSvgFilename() {
139+
return join(DOCS_DIR, 'results', 'preview.svg');
140+
}
141+
77142
function getNodeMajorVersion() {
78143
let majorVersion = 0;
79144

benchmarks/index.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
export { main } from './helpers/main';
1+
export {
2+
runAllBenchmarks,
3+
createPreviewGraph,
4+
deleteResults,
5+
} from './helpers/main';
26
export {
37
addCase,
48
AvailableBenchmarksIds,

cases/index.ts

+36-28
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,36 @@
1-
import './ajv';
2-
import './bueno';
3-
import './class-validator';
4-
import './computed-types';
5-
import './decoders';
6-
import './io-ts';
7-
import './jointz';
8-
import './json-decoder';
9-
import './marshal';
10-
import './mojotech-json-type-validation';
11-
import './myzod';
12-
import './ok-computer';
13-
import './purify-ts';
14-
import './rulr';
15-
import './runtypes';
16-
import './simple-runtypes';
17-
import './spectypes';
18-
import './superstruct';
19-
import './suretype';
20-
import './toi';
21-
import './tson';
22-
import './ts-interface-checker';
23-
import './ts-json-validator';
24-
import './ts-utils';
25-
import './typeofweb-schema';
26-
import './valita';
27-
import './yup';
28-
import './zod';
1+
export const cases = [
2+
'ajv',
3+
'bueno',
4+
'class-validator',
5+
'computed-types',
6+
'decoders',
7+
'io-ts',
8+
'jointz',
9+
'json-decoder',
10+
'marshal',
11+
'mojotech-json-type-validation',
12+
'myzod',
13+
'ok-computer',
14+
'purify-ts',
15+
'rulr',
16+
'runtypes',
17+
'simple-runtypes',
18+
'spectypes',
19+
'superstruct',
20+
'suretype',
21+
'toi',
22+
'ts-interface-checker',
23+
'ts-json-validator',
24+
'ts-utils',
25+
'tson',
26+
'typeofweb-schema',
27+
'valita',
28+
'yup',
29+
'zod',
30+
] as const;
31+
32+
export type CaseName = typeof cases[number];
33+
34+
export async function importCase(caseName: CaseName) {
35+
await import('./' + caseName);
36+
}

index.ts

+68-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,69 @@
1-
import { main } from './benchmarks';
2-
import './cases';
1+
import * as childProcess from 'child_process';
2+
import * as benchmarks from './benchmarks';
3+
import * as cases from './cases';
34

4-
main();
5+
async function main() {
6+
// a runtype lib would be handy here to check the passed command names ;)
7+
const [command, ...args] = process.argv.slice(2);
8+
9+
switch (command) {
10+
case undefined:
11+
case 'run':
12+
// run the given or all benchmarks, each in its own node process, see
13+
// https://github.com/moltar/typescript-runtime-type-benchmarks/issues/864
14+
{
15+
console.log('Removing previous results');
16+
benchmarks.deleteResults();
17+
18+
const caseNames = args.length ? args : cases.cases;
19+
20+
for (let c of caseNames) {
21+
if (c === 'spectypes') {
22+
// hack: manually run the spectypes compilation step - avoids
23+
// having to run it before any other benchmark, esp when working
24+
// locally and checking against a few selected ones.
25+
childProcess.execSync('npm run compile:spectypes', {
26+
stdio: 'inherit',
27+
});
28+
}
29+
30+
const cmd = [...process.argv.slice(0, 2), 'run-internal', c];
31+
32+
console.log('Executing "%s"', c);
33+
childProcess.execSync(cmd.join(' '), {
34+
stdio: 'inherit',
35+
});
36+
}
37+
}
38+
break;
39+
40+
case 'create-preview-svg':
41+
// separate command, because preview generation needs the accumulated
42+
// results from the benchmark runs
43+
await benchmarks.createPreviewGraph();
44+
break;
45+
46+
case 'run-internal':
47+
// run the given benchmark(s) & append the results
48+
{
49+
const caseNames = args as cases.CaseName[];
50+
51+
for (let c of caseNames) {
52+
console.log('Loading "%s"', c);
53+
54+
await cases.importCase(c);
55+
}
56+
57+
await benchmarks.runAllBenchmarks();
58+
}
59+
break;
60+
61+
default:
62+
console.error('unknown command:', command);
63+
process.exit(1);
64+
}
65+
}
66+
67+
main().catch(e => {
68+
throw e;
69+
});

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"scripts": {
1212
"lint": "gts check",
1313
"lint:fix": "gts fix",
14-
"start": "npm run compile:spectypes && ts-node index.ts",
14+
"start": "ts-node index.ts",
1515
"test:build": "npm run compile:spectypes && tsc --noEmit",
1616
"test": "npm run compile:spectypes && jest",
1717
"docs:serve": "serve docs",

start.sh

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ set -ex
44

55
export NODE_VERSION="${NODE_VERSION:-$(node -v)}"
66

7-
npm start
7+
npm run start
8+
npm run start create-preview-svg

test/benchmarks.test.ts

+44-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,48 @@
11
import { getRegisteredBenchmarks } from '../benchmarks';
2-
import '../cases';
2+
import { cases } from '../cases';
3+
4+
// all cases need to be imported here because jest cannot pic up dynamically
5+
// imported `test` and `describe`
6+
import '../cases/ajv';
7+
import '../cases/bueno';
8+
import '../cases/class-validator';
9+
import '../cases/computed-types';
10+
import '../cases/decoders';
11+
import '../cases/io-ts';
12+
import '../cases/jointz';
13+
import '../cases/json-decoder';
14+
import '../cases/marshal';
15+
import '../cases/mojotech-json-type-validation';
16+
import '../cases/myzod';
17+
import '../cases/ok-computer';
18+
import '../cases/purify-ts';
19+
import '../cases/rulr';
20+
import '../cases/runtypes';
21+
import '../cases/simple-runtypes';
22+
import '../cases/spectypes';
23+
import '../cases/superstruct';
24+
import '../cases/suretype';
25+
import '../cases/toi';
26+
import '../cases/ts-interface-checker';
27+
import '../cases/ts-json-validator';
28+
import '../cases/ts-utils';
29+
import '../cases/tson';
30+
import '../cases/typeofweb-schema';
31+
import '../cases/valita';
32+
import '../cases/yup';
33+
import '../cases/zod';
34+
35+
test('all cases must have been imported in tests', () => {
36+
const registeredCases = new Set<string>();
37+
38+
getRegisteredBenchmarks().forEach(([benchmarkId, benchmarkCases]) => {
39+
benchmarkCases.forEach(b => {
40+
registeredCases.add(b.moduleName);
41+
});
42+
});
43+
44+
expect(registeredCases.size).toEqual(cases.length);
45+
});
346

447
getRegisteredBenchmarks().forEach(([benchmarkId, benchmarkCases]) => {
548
describe(benchmarkId, () => {

0 commit comments

Comments
 (0)