Skip to content

Commit f32f3d0

Browse files
authored
Simplify benchmark framework (#161)
1 parent acfa5ee commit f32f3d0

3 files changed

Lines changed: 91 additions & 187 deletions

File tree

packages/protovalidate-bench/README.md

Lines changed: 11 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -6,95 +6,29 @@ so that runtime cost can be tracked across changes and compared cross-language.
66

77
## Running
88

9-
From the repo root:
9+
With turborepo:
1010

1111
```shell
12-
npx turbo run bench [regex] -d dir
12+
npx turbo run bench -- [regex] --dir <dir>
1313
```
1414

15-
Or from this directory:
15+
With npm (make sure to generate proto and build dependencies first):
1616

1717
```shell
1818
npm run bench
1919
```
2020

2121
The runner prints a table of results and writes a JSON file to `.tmp/bench/`
22-
(gitignored) named after the current timestamp.
22+
(gitignored) named after the current timestamp. See type `OutputJson` for the
23+
file contents.
2324

24-
The regular expression is optional and only runs tasks whose name contains it.
25+
### Arguments and options
2526

26-
### Options
27-
28-
| Flag | Default | Description |
29-
|------------|--------------|-----------------------------------|
30-
| `-d <dir>` | `.tmp/bench` | Output directory for JSON results |
31-
| `-h` | | Print usage information |
32-
33-
### Output schema
34-
35-
Each invocation writes a single JSON file named after the current timestamp,
36-
containing a `tasks` array of objects with the following fields:
37-
38-
```json
39-
{
40-
"timestamp": "2026-06-02T17-30-42-845Z",
41-
"node": "v24.15.0",
42-
"platform": "darwin/arm64",
43-
"tasks": [
44-
{
45-
"name": "Scalar",
46-
"result": {
47-
"state": "completed",
48-
"latency": {
49-
"aad": 0.000023186290713732412,
50-
"critical": 1.96,
51-
"df": 217917,
52-
"mad": 9.99999883788405e-7,
53-
"max": 0.10383299999989504,
54-
"mean": 0.0004588884259220391,
55-
"min": 0.00033300000018243736,
56-
"moe": 0.000001330994271845705,
57-
"p50": 0.00045799999998052954,
58-
"p75": 0.00045899999986431794,
59-
"p99": 0.0005840000001171575,
60-
"p995": 0.0007499999999254214,
61-
"p999": 0.0014590000000680448,
62-
"rme": 0.2900474705090576,
63-
"samplesCount": 217918,
64-
"sd": 0.0003170054051330531,
65-
"sem": 6.790787101253597e-7,
66-
"variance": 1.0049242688357114e-7
67-
},
68-
"period": 0.0004588884259220385,
69-
"throughput": {
70-
"aad": 97984.76021053715,
71-
"critical": 1.96,
72-
"df": 217917,
73-
"mad": 4756.875513155013,
74-
"max": 3003003.001357778,
75-
"mean": 2215598.5707707237,
76-
"min": 9630.849537247415,
77-
"moe": 672.4829553507935,
78-
"p50": 2183406.1136299386,
79-
"p75": 2398081.533635069,
80-
"p99": 2403846.154659788,
81-
"p995": 2403846.154659788,
82-
"p999": 2403846.154659788,
83-
"rme": 0.030352202074081583,
84-
"samplesCount": 217918,
85-
"sd": 160166.5282980001,
86-
"sem": 343.10354864836404,
87-
"variance": 25653316787.034073
88-
},
89-
"totalTime": 100.00004800007878,
90-
"runtime": "node",
91-
"runtimeVersion": "24.15.0",
92-
"timestampProviderName": "performanceNow"
93-
}
94-
}
95-
]
96-
}
97-
```
27+
| Argument / flag | Default | Description |
28+
|-----------------|--------------|------------------------------------|
29+
| `[regex]` | `.*` | Run tasks matching this regex. |
30+
| `--dir <dir>` | `.tmp/bench` | Output directory for JSON results. |
31+
| `--help`, `-h` | | Print usage information. |
9832

9933
## Benchmarks
10034

@@ -118,13 +52,3 @@ between languages stay meaningful.
11852
| `MultiRule/NoError` | Same schema, valid value — success path. |
11953
| `StandardSchema/Scalar` | Standard Schema adapter, scalar message. TS-only — no Go analogue. |
12054
| `StandardSchema/ComplexSchema` | Standard Schema adapter, complex message. |
121-
122-
## Regenerating proto code
123-
124-
If the `.proto` files change:
125-
126-
```shell
127-
npm run generate
128-
```
129-
130-
Generated code lives under `src/gen/` and is committed.

packages/protovalidate-bench/src/bench.ts

Lines changed: 79 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -12,122 +12,102 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import { Bench } from "tinybench";
1615
import * as console from "node:console";
17-
import type { DescMessage, Message } from "@bufbuild/protobuf";
18-
import { createValidator } from "@bufbuild/protovalidate";
19-
import { cases } from "./cases.js";
2016
import { writeFileSync } from "node:fs";
2117
import { parseArgs } from "node:util";
18+
import { createValidator } from "@bufbuild/protovalidate";
19+
import { Bench, type Task } from "tinybench";
20+
import { cases } from "./cases.js";
2221

23-
/* eslint-disable no-console, import/no-named-as-default-member */
24-
25-
let outPath = ".tmp/bench";
26-
27-
async function main(args: string[]): Promise<void> {
28-
function filterTests(regexp: string): Test[] {
29-
const tests = setupTests();
30-
const re = new RegExp(regexp);
31-
return tests.filter((test) => re.test(test.name));
32-
}
33-
34-
const options = {
35-
dir: {
36-
type: "string",
37-
},
38-
help: {
39-
type: "boolean",
40-
short: "h",
41-
},
42-
} as const;
43-
44-
const { values, positionals } = parseArgs({
45-
options,
46-
allowPositionals: true,
47-
});
48-
if (values.help) {
49-
exitUsage(0);
50-
}
51-
if (values.dir) {
52-
outPath = values.dir;
53-
}
54-
if (positionals.length > 1) {
55-
exitUsage(2);
56-
}
57-
58-
let filter = ".*";
59-
if (positionals.length == 1) {
60-
filter = positionals[0];
61-
}
62-
const tests = filterTests(filter);
63-
if (tests.length == 0) {
64-
console.log("No tests match pattern; exiting.");
65-
process.exit(0);
66-
}
67-
await bench(tests);
68-
69-
function exitUsage(exitCode = 0): never {
70-
const out = exitCode === 0 ? process.stdout : process.stderr;
71-
out.write(
72-
[
73-
`USAGE: ${process.argv[1]} [regex]`,
74-
``,
75-
`Run tests with the npm package "tinybench", and print results to standard out.`,
76-
`If no regex is supplied, all benchmarks are run.`,
77-
``,
78-
].join("\n"),
79-
);
80-
process.exit(exitCode);
81-
}
22+
const usage = `USAGE: ${process.argv[1]} [regex]
23+
24+
Run tests with the npm package "tinybench", and print results to standard out.
25+
If no regex is supplied, all benchmarks are run.
26+
27+
Arguments:
28+
regex Run only tests whose name matches this regex.
29+
30+
Options:
31+
--dir <dir> Directory for JSON results (default: .tmp/bench).
32+
-h, --help Print this help and exit.
33+
`;
34+
35+
const options = {
36+
dir: {
37+
type: "string",
38+
},
39+
help: {
40+
type: "boolean",
41+
short: "h",
42+
},
43+
} as const;
44+
const { values, positionals } = parseArgs({
45+
options,
46+
allowPositionals: true,
47+
});
48+
if (values.help) {
49+
console.log(usage);
50+
process.exit(0);
8251
}
83-
84-
interface Test {
85-
name: string;
86-
schema: DescMessage;
87-
fixture: Message;
52+
if (positionals.length > 1) {
53+
console.error(usage);
54+
process.exit(2);
55+
}
56+
const outPath = values.dir ?? ".tmp/bench";
57+
const filter = positionals.length > 0 ? new RegExp(positionals[0]) : /.*/;
58+
const tests = cases.filter((test) => filter.test(test.name));
59+
if (tests.length == 0) {
60+
console.log("No tests match pattern; exiting.");
61+
process.exit(0);
8862
}
8963

90-
function setupTests(): Test[] {
91-
const tests: Test[] = [];
92-
tests.push(...cases);
93-
return tests;
64+
const bench = new Bench({ name: "protovalidate benchmarks", time: 100 });
65+
const validator = createValidator();
66+
for (const test of tests) {
67+
bench.add(test.name, () => {
68+
validator.validate(test.schema, test.fixture);
69+
});
9470
}
71+
await bench.run();
72+
writeOutputJson(outPath, bench.tasks);
73+
console.log(bench.name);
74+
console.table(bench.table());
9575

9676
/**
97-
* Benchmark tests with the npm package "tinybench". Results are printed to
98-
* standard out.
77+
* JSON output
9978
*/
100-
async function bench(tests: Test[]): Promise<void> {
101-
const bench = new Bench({ name: "protovalidate benchmarks", time: 100 });
102-
const validator = createValidator();
103-
104-
for (const test of tests) {
105-
bench.add(test.name, () => {
106-
validator.validate(test.schema, test.fixture);
107-
});
108-
}
109-
110-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
111-
112-
await bench.run();
113-
114-
const payload = {
115-
timestamp: timestamp,
79+
type OutputJson = {
80+
timestamp: Date;
81+
/**
82+
* Node.js version
83+
*/
84+
node: string;
85+
/**
86+
* Node.js platform / arch
87+
*/
88+
platform: string;
89+
/**
90+
* tinybench task results
91+
*/
92+
tasks: {
93+
name: string;
94+
result: Task["result"];
95+
}[];
96+
};
97+
98+
function writeOutputJson(outPath: string, tasks: Task[]) {
99+
const timestamp = new Date();
100+
const output: OutputJson = {
101+
timestamp,
116102
node: process.version,
117103
platform: `${process.platform}/${process.arch}`,
118-
tasks: bench.tasks.map((t) => ({
104+
tasks: tasks.map((t) => ({
119105
name: t.name,
120-
// t.result is undefined if the task errored
121106
result: t.result,
122107
})),
123108
};
124109
writeFileSync(
125-
`${outPath}/${timestamp}.json`,
126-
JSON.stringify(payload, null, 2),
110+
`${outPath}/${timestamp.toISOString().replace(/[:.]/g, "-")}.json`,
111+
JSON.stringify(output, null, 2),
127112
);
128-
129-
console.log(bench.name);
130-
console.table(bench.table());
131113
}
132-
133-
await main(process.argv.slice(2));

packages/protovalidate-bench/src/cases.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ import {
4848
/**
4949
* One bench case: a schema, a fixture, and the name to record under.
5050
*/
51-
export type BenchCase = {
51+
type BenchCase = {
5252
name: string;
5353
schema: DescMessage;
5454
fixture: Message;

0 commit comments

Comments
 (0)