modestbench 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +45 -0
- package/LICENSE.md +55 -0
- package/README.md +699 -0
- package/dist/bootstrap.cjs +37 -0
- package/dist/bootstrap.cjs.map +1 -0
- package/dist/bootstrap.d.cts +17 -0
- package/dist/bootstrap.d.cts.map +1 -0
- package/dist/bootstrap.d.ts +17 -0
- package/dist/bootstrap.d.ts.map +1 -0
- package/dist/bootstrap.js +33 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/cli/commands/history.cjs +459 -0
- package/dist/cli/commands/history.cjs.map +1 -0
- package/dist/cli/commands/history.d.cts +34 -0
- package/dist/cli/commands/history.d.cts.map +1 -0
- package/dist/cli/commands/history.d.ts +34 -0
- package/dist/cli/commands/history.d.ts.map +1 -0
- package/dist/cli/commands/history.js +422 -0
- package/dist/cli/commands/history.js.map +1 -0
- package/dist/cli/commands/init.cjs +566 -0
- package/dist/cli/commands/init.cjs.map +1 -0
- package/dist/cli/commands/init.d.cts +26 -0
- package/dist/cli/commands/init.d.cts.map +1 -0
- package/dist/cli/commands/init.d.ts +26 -0
- package/dist/cli/commands/init.d.ts.map +1 -0
- package/dist/cli/commands/init.js +562 -0
- package/dist/cli/commands/init.js.map +1 -0
- package/dist/cli/commands/run.cjs +285 -0
- package/dist/cli/commands/run.cjs.map +1 -0
- package/dist/cli/commands/run.d.cts +37 -0
- package/dist/cli/commands/run.d.cts.map +1 -0
- package/dist/cli/commands/run.d.ts +37 -0
- package/dist/cli/commands/run.d.ts.map +1 -0
- package/dist/cli/commands/run.js +248 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/index.cjs +523 -0
- package/dist/cli/index.cjs.map +1 -0
- package/dist/cli/index.d.cts +58 -0
- package/dist/cli/index.d.cts.map +1 -0
- package/dist/cli/index.d.ts +58 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +515 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/config/manager.cjs +370 -0
- package/dist/config/manager.cjs.map +1 -0
- package/dist/config/manager.d.cts +46 -0
- package/dist/config/manager.d.cts.map +1 -0
- package/dist/config/manager.d.ts +46 -0
- package/dist/config/manager.d.ts.map +1 -0
- package/dist/config/manager.js +333 -0
- package/dist/config/manager.js.map +1 -0
- package/dist/config/schema.cjs +182 -0
- package/dist/config/schema.cjs.map +1 -0
- package/dist/config/schema.d.cts +51 -0
- package/dist/config/schema.d.cts.map +1 -0
- package/dist/config/schema.d.ts +51 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +145 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/constants.cjs +22 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.d.cts +10 -0
- package/dist/constants.d.cts.map +1 -0
- package/dist/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +19 -0
- package/dist/constants.js.map +1 -0
- package/dist/core/benchmark-schema.cjs +135 -0
- package/dist/core/benchmark-schema.cjs.map +1 -0
- package/dist/core/benchmark-schema.d.cts +139 -0
- package/dist/core/benchmark-schema.d.cts.map +1 -0
- package/dist/core/benchmark-schema.d.ts +139 -0
- package/dist/core/benchmark-schema.d.ts.map +1 -0
- package/dist/core/benchmark-schema.js +132 -0
- package/dist/core/benchmark-schema.js.map +1 -0
- package/dist/core/engine.cjs +669 -0
- package/dist/core/engine.cjs.map +1 -0
- package/dist/core/engine.d.cts +128 -0
- package/dist/core/engine.d.cts.map +1 -0
- package/dist/core/engine.d.ts +128 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +632 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/engines/accurate-engine.cjs +292 -0
- package/dist/core/engines/accurate-engine.cjs.map +1 -0
- package/dist/core/engines/accurate-engine.d.cts +63 -0
- package/dist/core/engines/accurate-engine.d.cts.map +1 -0
- package/dist/core/engines/accurate-engine.d.ts +63 -0
- package/dist/core/engines/accurate-engine.d.ts.map +1 -0
- package/dist/core/engines/accurate-engine.js +288 -0
- package/dist/core/engines/accurate-engine.js.map +1 -0
- package/dist/core/engines/index.cjs +21 -0
- package/dist/core/engines/index.cjs.map +1 -0
- package/dist/core/engines/index.d.cts +16 -0
- package/dist/core/engines/index.d.cts.map +1 -0
- package/dist/core/engines/index.d.ts +16 -0
- package/dist/core/engines/index.d.ts.map +1 -0
- package/dist/core/engines/index.js +16 -0
- package/dist/core/engines/index.js.map +1 -0
- package/dist/core/engines/tinybench-engine.cjs +286 -0
- package/dist/core/engines/tinybench-engine.cjs.map +1 -0
- package/dist/core/engines/tinybench-engine.d.cts +18 -0
- package/dist/core/engines/tinybench-engine.d.cts.map +1 -0
- package/dist/core/engines/tinybench-engine.d.ts +18 -0
- package/dist/core/engines/tinybench-engine.d.ts.map +1 -0
- package/dist/core/engines/tinybench-engine.js +282 -0
- package/dist/core/engines/tinybench-engine.js.map +1 -0
- package/dist/core/error-manager.cjs +303 -0
- package/dist/core/error-manager.cjs.map +1 -0
- package/dist/core/error-manager.d.cts +77 -0
- package/dist/core/error-manager.d.cts.map +1 -0
- package/dist/core/error-manager.d.ts +77 -0
- package/dist/core/error-manager.d.ts.map +1 -0
- package/dist/core/error-manager.js +299 -0
- package/dist/core/error-manager.js.map +1 -0
- package/dist/core/loader.cjs +287 -0
- package/dist/core/loader.cjs.map +1 -0
- package/dist/core/loader.d.cts +55 -0
- package/dist/core/loader.d.cts.map +1 -0
- package/dist/core/loader.d.ts +55 -0
- package/dist/core/loader.d.ts.map +1 -0
- package/dist/core/loader.js +250 -0
- package/dist/core/loader.js.map +1 -0
- package/dist/core/stats-utils.cjs +99 -0
- package/dist/core/stats-utils.cjs.map +1 -0
- package/dist/core/stats-utils.d.cts +50 -0
- package/dist/core/stats-utils.d.cts.map +1 -0
- package/dist/core/stats-utils.d.ts +50 -0
- package/dist/core/stats-utils.d.ts.map +1 -0
- package/dist/core/stats-utils.js +94 -0
- package/dist/core/stats-utils.js.map +1 -0
- package/dist/index.cjs +64 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +22 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/progress/manager.cjs +325 -0
- package/dist/progress/manager.cjs.map +1 -0
- package/dist/progress/manager.d.cts +125 -0
- package/dist/progress/manager.d.cts.map +1 -0
- package/dist/progress/manager.d.ts +125 -0
- package/dist/progress/manager.d.ts.map +1 -0
- package/dist/progress/manager.js +321 -0
- package/dist/progress/manager.js.map +1 -0
- package/dist/reporters/csv.cjs +250 -0
- package/dist/reporters/csv.cjs.map +1 -0
- package/dist/reporters/csv.d.cts +92 -0
- package/dist/reporters/csv.d.cts.map +1 -0
- package/dist/reporters/csv.d.ts +92 -0
- package/dist/reporters/csv.d.ts.map +1 -0
- package/dist/reporters/csv.js +246 -0
- package/dist/reporters/csv.js.map +1 -0
- package/dist/reporters/human.cjs +516 -0
- package/dist/reporters/human.cjs.map +1 -0
- package/dist/reporters/human.d.cts +86 -0
- package/dist/reporters/human.d.cts.map +1 -0
- package/dist/reporters/human.d.ts +86 -0
- package/dist/reporters/human.d.ts.map +1 -0
- package/dist/reporters/human.js +509 -0
- package/dist/reporters/human.js.map +1 -0
- package/dist/reporters/index.cjs +17 -0
- package/dist/reporters/index.cjs.map +1 -0
- package/dist/reporters/index.d.cts +10 -0
- package/dist/reporters/index.d.cts.map +1 -0
- package/dist/reporters/index.d.ts +10 -0
- package/dist/reporters/index.d.ts.map +1 -0
- package/dist/reporters/index.js +10 -0
- package/dist/reporters/index.js.map +1 -0
- package/dist/reporters/json.cjs +215 -0
- package/dist/reporters/json.cjs.map +1 -0
- package/dist/reporters/json.d.cts +79 -0
- package/dist/reporters/json.d.cts.map +1 -0
- package/dist/reporters/json.d.ts +79 -0
- package/dist/reporters/json.d.ts.map +1 -0
- package/dist/reporters/json.js +211 -0
- package/dist/reporters/json.js.map +1 -0
- package/dist/reporters/registry.cjs +255 -0
- package/dist/reporters/registry.cjs.map +1 -0
- package/dist/reporters/registry.d.cts +155 -0
- package/dist/reporters/registry.d.cts.map +1 -0
- package/dist/reporters/registry.d.ts +155 -0
- package/dist/reporters/registry.d.ts.map +1 -0
- package/dist/reporters/registry.js +249 -0
- package/dist/reporters/registry.js.map +1 -0
- package/dist/reporters/simple.cjs +328 -0
- package/dist/reporters/simple.cjs.map +1 -0
- package/dist/reporters/simple.d.cts +51 -0
- package/dist/reporters/simple.d.cts.map +1 -0
- package/dist/reporters/simple.d.ts +51 -0
- package/dist/reporters/simple.d.ts.map +1 -0
- package/dist/reporters/simple.js +321 -0
- package/dist/reporters/simple.js.map +1 -0
- package/dist/schema/modestbench-config.schema.json +162 -0
- package/dist/storage/history.cjs +456 -0
- package/dist/storage/history.cjs.map +1 -0
- package/dist/storage/history.d.cts +99 -0
- package/dist/storage/history.d.cts.map +1 -0
- package/dist/storage/history.d.ts +99 -0
- package/dist/storage/history.d.ts.map +1 -0
- package/dist/storage/history.js +452 -0
- package/dist/storage/history.js.map +1 -0
- package/dist/types/cli.cjs +21 -0
- package/dist/types/cli.cjs.map +1 -0
- package/dist/types/cli.d.cts +296 -0
- package/dist/types/cli.d.cts.map +1 -0
- package/dist/types/cli.d.ts +296 -0
- package/dist/types/cli.d.ts.map +1 -0
- package/dist/types/cli.js +18 -0
- package/dist/types/cli.js.map +1 -0
- package/dist/types/core.cjs +14 -0
- package/dist/types/core.cjs.map +1 -0
- package/dist/types/core.d.cts +380 -0
- package/dist/types/core.d.cts.map +1 -0
- package/dist/types/core.d.ts +380 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/types/core.js +13 -0
- package/dist/types/core.js.map +1 -0
- package/dist/types/index.cjs +27 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.cts +11 -0
- package/dist/types/index.d.cts.map +1 -0
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +11 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/interfaces.cjs +10 -0
- package/dist/types/interfaces.cjs.map +1 -0
- package/dist/types/interfaces.d.cts +381 -0
- package/dist/types/interfaces.d.cts.map +1 -0
- package/dist/types/interfaces.d.ts +381 -0
- package/dist/types/interfaces.d.ts.map +1 -0
- package/dist/types/interfaces.js +9 -0
- package/dist/types/interfaces.js.map +1 -0
- package/dist/types/utility.cjs +92 -0
- package/dist/types/utility.cjs.map +1 -0
- package/dist/types/utility.d.cts +330 -0
- package/dist/types/utility.d.cts.map +1 -0
- package/dist/types/utility.d.ts +330 -0
- package/dist/types/utility.d.ts.map +1 -0
- package/dist/types/utility.js +78 -0
- package/dist/types/utility.js.map +1 -0
- package/package.json +211 -0
- package/src/bootstrap.ts +35 -0
- package/src/cli/commands/history.ts +569 -0
- package/src/cli/commands/init.ts +658 -0
- package/src/cli/commands/run.ts +346 -0
- package/src/cli/index.ts +642 -0
- package/src/config/manager.ts +387 -0
- package/src/config/schema.ts +188 -0
- package/src/constants.ts +21 -0
- package/src/core/benchmark-schema.ts +185 -0
- package/src/core/engine.ts +888 -0
- package/src/core/engines/accurate-engine.ts +408 -0
- package/src/core/engines/index.ts +16 -0
- package/src/core/engines/tinybench-engine.ts +335 -0
- package/src/core/error-manager.ts +372 -0
- package/src/core/loader.ts +324 -0
- package/src/core/stats-utils.ts +135 -0
- package/src/index.ts +46 -0
- package/src/progress/manager.ts +415 -0
- package/src/reporters/csv.ts +368 -0
- package/src/reporters/human.ts +707 -0
- package/src/reporters/index.ts +10 -0
- package/src/reporters/json.ts +302 -0
- package/src/reporters/registry.ts +349 -0
- package/src/reporters/simple.ts +459 -0
- package/src/storage/history.ts +600 -0
- package/src/types/cli.ts +312 -0
- package/src/types/core.ts +414 -0
- package/src/types/index.ts +18 -0
- package/src/types/interfaces.ts +451 -0
- package/src/types/utility.ts +446 -0
package/README.md
ADDED
|
@@ -0,0 +1,699 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="/"><img src="./assets/logo-512.png" width="512px" align="center" alt="modestbench: a full-ass benchmarking framework for Node.js"/></a>
|
|
3
|
+
<h1 align="center"><span class="modestbench"><code>modestbench</code><span></h1>
|
|
4
|
+
<p align="center">
|
|
5
|
+
<em>“A full-ass benchmarking framework for Node.js”</em>
|
|
6
|
+
<br/>
|
|
7
|
+
<small>by <a href="https://github.com/boneskull" title="@boneskull on GitHub">@boneskull</a></small>
|
|
8
|
+
</p>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **Fast & Accurate**: High-precision timing with statistical analysis
|
|
14
|
+
- **Multiple Output Formats**: Human-readable, JSON, and CSV reports (at the same time!!)
|
|
15
|
+
- **Historical Tracking**: Store and compare benchmark results over time
|
|
16
|
+
- **Tagging System**: Organize and filter benchmarks by categories
|
|
17
|
+
- **CLI & API**: Command-line interface and programmatic API
|
|
18
|
+
- **TypeScript Support**: Full type safety
|
|
19
|
+
|
|
20
|
+
In summary, **modestbench** wraps [tinybench][] and enhances it with a bunch of crap so you don't have to think.
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Installation
|
|
25
|
+
|
|
26
|
+
The usual suspects:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install --save-dev modestbench
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Optional: Initialize a Project
|
|
33
|
+
|
|
34
|
+
The `modestbench` CLI provides a `init` command. This command:
|
|
35
|
+
|
|
36
|
+
1. Generates a configuration file in a format of your choosing
|
|
37
|
+
2. Creates an example benchmark file
|
|
38
|
+
3. Appends `.modestbench/` to `.gitignore` to exclude historical data from version control
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Initialize with examples and configuration
|
|
42
|
+
modestbench init
|
|
43
|
+
|
|
44
|
+
# Or specify project type and config format
|
|
45
|
+
modestbench init advanced --config-type typescript
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Project Types:**
|
|
49
|
+
|
|
50
|
+
- `basic` - Simple setup for small projects (100 iterations, human reporter)
|
|
51
|
+
- `advanced` - Feature-rich with multiple reporters and structured output (1000 iterations, warmup, human + JSON reporters)
|
|
52
|
+
- `library` - Optimized for library performance testing (5000 iterations, high warmup, human + JSON reporters, organized suite structure)
|
|
53
|
+
|
|
54
|
+
### My First Benchmark
|
|
55
|
+
|
|
56
|
+
> **_PRO TIP_**: The convention for **modestbench** benchmark files is to use the `.bench.js` or `.bench.ts` extension.
|
|
57
|
+
|
|
58
|
+
**modestbench** supports two formats for defining benchmarks:
|
|
59
|
+
|
|
60
|
+
#### Simplified Format (Recommended for Simple Cases)
|
|
61
|
+
|
|
62
|
+
For quick benchmarks with just a few tasks, you can use the simplified format:
|
|
63
|
+
|
|
64
|
+
```javascript
|
|
65
|
+
// benchmarks/example.bench.js
|
|
66
|
+
export default {
|
|
67
|
+
'Array.push()': () => {
|
|
68
|
+
const arr = [];
|
|
69
|
+
for (let i = 0; i < 1000; i++) {
|
|
70
|
+
arr.push(i);
|
|
71
|
+
}
|
|
72
|
+
return arr;
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
'Array spread': () => {
|
|
76
|
+
let arr = [];
|
|
77
|
+
for (let i = 0; i < 1000; i++) {
|
|
78
|
+
arr = [...arr, i];
|
|
79
|
+
}
|
|
80
|
+
return arr;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Suite-Based Format (For Complex Projects)
|
|
86
|
+
|
|
87
|
+
When you need to organize benchmarks into groups with setup/teardown hooks:
|
|
88
|
+
|
|
89
|
+
```javascript
|
|
90
|
+
// benchmarks/example.bench.js
|
|
91
|
+
export default {
|
|
92
|
+
suites: {
|
|
93
|
+
'Array Operations': {
|
|
94
|
+
benchmarks: {
|
|
95
|
+
'Array.push()': () => {
|
|
96
|
+
const arr = [];
|
|
97
|
+
for (let i = 0; i < 1000; i++) {
|
|
98
|
+
arr.push(i);
|
|
99
|
+
}
|
|
100
|
+
return arr;
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
'Array spread': () => {
|
|
104
|
+
let arr = [];
|
|
105
|
+
for (let i = 0; i < 1000; i++) {
|
|
106
|
+
arr = [...arr, i];
|
|
107
|
+
}
|
|
108
|
+
return arr;
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**When to use each format:**
|
|
117
|
+
|
|
118
|
+
> - **Simplified format**: Quick benchmarks, single file with related tasks, no setup/teardown needed
|
|
119
|
+
> - **Suite format**: Complex projects, multiple groups of benchmarks, need setup/teardown hooks, or want explicit organization
|
|
120
|
+
|
|
121
|
+
### Running Your First Benchmarks
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Run all benchmarks
|
|
125
|
+
modestbench run
|
|
126
|
+
|
|
127
|
+
# Run with specific options
|
|
128
|
+
modestbench run --iterations 5000 --reporters human,json
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### View Results
|
|
132
|
+
|
|
133
|
+
```text
|
|
134
|
+
🚀 ModestBench
|
|
135
|
+
|
|
136
|
+
Environment:
|
|
137
|
+
Node.js: v24.10.0
|
|
138
|
+
Platform: darwin arm64
|
|
139
|
+
CPU: Apple M4 Max (16 cores)
|
|
140
|
+
Memory: 48.0 GB
|
|
141
|
+
|
|
142
|
+
Found 1 benchmark file(s)
|
|
143
|
+
|
|
144
|
+
▶ benchmarks/example.bench.js
|
|
145
|
+
|
|
146
|
+
▶ Array Operations
|
|
147
|
+
✓ Array.push()
|
|
148
|
+
810.05μs ±2.45% (1.23M ops/sec)
|
|
149
|
+
✓ Array spread
|
|
150
|
+
81.01ms ±4.12% (12.34K ops/sec)
|
|
151
|
+
✓ 2 passed
|
|
152
|
+
|
|
153
|
+
✓ All 2 tasks passed
|
|
154
|
+
|
|
155
|
+
📊 Results
|
|
156
|
+
|
|
157
|
+
✓ All tests passed: 2
|
|
158
|
+
📁 Files: 1
|
|
159
|
+
📊 Suites: 1
|
|
160
|
+
⏱️ Duration: 1.82s
|
|
161
|
+
|
|
162
|
+
🎉 All benchmarks completed successfully!
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Getting Started
|
|
166
|
+
|
|
167
|
+
Jump to:
|
|
168
|
+
|
|
169
|
+
- [Quick Start](#quick-start) - Basic concepts and your first benchmark
|
|
170
|
+
- [Configuration](#configuration) - Project and runtime configuration options
|
|
171
|
+
- [Advanced Features](#advanced-features) - Multiple suites, async operations, and tagging
|
|
172
|
+
- [Integration Examples](#integration-examples) - CI/CD integration and performance monitoring
|
|
173
|
+
- [Programmatic API](#programmatic-api) - Using **modestbench** programmatically
|
|
174
|
+
|
|
175
|
+
See the **[examples directory](examples/README.md)** for additional guides and sample code.
|
|
176
|
+
|
|
177
|
+
## CLI Commands
|
|
178
|
+
|
|
179
|
+
### Run Benchmarks
|
|
180
|
+
|
|
181
|
+
Run benchmarks with sensible defaults:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# Run benchmarks in current directory and bench/ (top-level only)
|
|
185
|
+
modestbench run
|
|
186
|
+
|
|
187
|
+
# Run all benchmarks in a directory (searches recursively)
|
|
188
|
+
modestbench run benchmarks/
|
|
189
|
+
|
|
190
|
+
# Run benchmarks from multiple directories
|
|
191
|
+
modestbench run src/perf/ tests/benchmarks/
|
|
192
|
+
|
|
193
|
+
# Run specific files
|
|
194
|
+
modestbench run benchmarks/critical.bench.js
|
|
195
|
+
|
|
196
|
+
# Mix files, directories, and glob patterns
|
|
197
|
+
modestbench run file.bench.js benchmarks/ "tests/**/*.bench.ts"
|
|
198
|
+
|
|
199
|
+
# With options
|
|
200
|
+
modestbench run \
|
|
201
|
+
--config ./config.json \
|
|
202
|
+
--iterations 2000 \
|
|
203
|
+
--reporters human,json,csv \
|
|
204
|
+
--output ./results \
|
|
205
|
+
--tags performance,algorithm \
|
|
206
|
+
--concurrent
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Supported file extensions:**
|
|
210
|
+
|
|
211
|
+
- JavaScript: `.js`, `.mjs`, `.cjs`
|
|
212
|
+
- TypeScript: `.ts`, `.mts`, `.cts`
|
|
213
|
+
|
|
214
|
+
#### Controlling Benchmark Limits
|
|
215
|
+
|
|
216
|
+
The `--limit-by` flag controls whether benchmarks are limited by time, iteration count, or both:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Limit by iteration count (fast, predictable sample size)
|
|
220
|
+
modestbench run --iterations 100
|
|
221
|
+
|
|
222
|
+
# Limit by time budget (ensures consistent time investment)
|
|
223
|
+
modestbench run --time 5000
|
|
224
|
+
|
|
225
|
+
# Limit by whichever comes first (safety bounds)
|
|
226
|
+
modestbench run --iterations 1000 --time 10000
|
|
227
|
+
|
|
228
|
+
# Explicit control (overrides smart defaults)
|
|
229
|
+
modestbench run --iterations 500 --time 5000 --limit-by time
|
|
230
|
+
|
|
231
|
+
# Require both thresholds (rare, for statistical rigor)
|
|
232
|
+
modestbench run --iterations 100 --time 2000 --limit-by all
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**Smart Defaults:**
|
|
236
|
+
|
|
237
|
+
- Only `--iterations` provided → limits by iteration count (fast)
|
|
238
|
+
- Only `--time` provided → limits by time budget
|
|
239
|
+
- Both provided → stops at whichever comes first (`any` mode)
|
|
240
|
+
- Neither provided → uses default iterations (100) with iterations mode
|
|
241
|
+
|
|
242
|
+
**Modes:**
|
|
243
|
+
|
|
244
|
+
- `iterations`: Stop after N samples (time budget set to 1ms)
|
|
245
|
+
- `time`: Run for T milliseconds (collect as many samples as possible)
|
|
246
|
+
- `any`: Stop when either threshold is reached (defaults to iterations behavior for fast completion)
|
|
247
|
+
- `all`: Require both time AND iterations thresholds (tinybench default behavior)
|
|
248
|
+
|
|
249
|
+
#### Filtering by Tags
|
|
250
|
+
|
|
251
|
+
Run specific subsets of benchmarks using tags:
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
# Run only benchmarks tagged with 'fast'
|
|
255
|
+
modestbench run --tags fast
|
|
256
|
+
|
|
257
|
+
# Run benchmarks with multiple tags (OR logic - matches ANY)
|
|
258
|
+
modestbench run --tags string,array,algorithm
|
|
259
|
+
|
|
260
|
+
# Exclude specific benchmarks
|
|
261
|
+
modestbench run --exclude-tags slow,experimental
|
|
262
|
+
|
|
263
|
+
# Combine: run fast benchmarks except experimental ones
|
|
264
|
+
modestbench run --tags fast --exclude-tags experimental
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Key Features:**
|
|
268
|
+
|
|
269
|
+
- Tags cascade from file → suite → task levels
|
|
270
|
+
- `--tags` uses OR logic (matches ANY specified tag)
|
|
271
|
+
- `--exclude-tags` takes precedence over `--tags`
|
|
272
|
+
- Suite setup/teardown only runs if at least one task matches
|
|
273
|
+
|
|
274
|
+
See [Tagging and Filtering](#tagging-and-filtering) for detailed examples.
|
|
275
|
+
|
|
276
|
+
### History Management
|
|
277
|
+
|
|
278
|
+
**modestbench** automatically tracks benchmark results over time in a local `.modestbench/` directory. This history enables you to:
|
|
279
|
+
|
|
280
|
+
- **Track performance trends** - See how your code's performance changes across commits
|
|
281
|
+
- **Detect regressions** - Compare current results against previous runs to catch slowdowns
|
|
282
|
+
- **Analyze improvements** - Validate that optimizations actually improve performance
|
|
283
|
+
- **Document progress** - Export historical data for reports and analysis
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
# List recent runs
|
|
287
|
+
modestbench history list
|
|
288
|
+
|
|
289
|
+
# Show detailed results
|
|
290
|
+
modestbench history show <run-id>
|
|
291
|
+
|
|
292
|
+
# Compare two runs
|
|
293
|
+
modestbench history compare <run-id-1> <run-id-2>
|
|
294
|
+
|
|
295
|
+
# Export historical data
|
|
296
|
+
modestbench history export --format csv --output results.csv
|
|
297
|
+
|
|
298
|
+
# Clean old data
|
|
299
|
+
modestbench history clean --older-than 30d
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
## Configuration
|
|
303
|
+
|
|
304
|
+
### Project Configuration
|
|
305
|
+
|
|
306
|
+
Create `modestbench.config.json`:
|
|
307
|
+
|
|
308
|
+
```jsonc
|
|
309
|
+
{
|
|
310
|
+
"bail": false, // Stop execution on first failure
|
|
311
|
+
"exclude": ["node_modules/**"], // Patterns to exclude from discovery
|
|
312
|
+
"excludeTags": ["slow", "experimental"], // Tags to exclude from execution
|
|
313
|
+
"iterations": 1000, // Number of samples per benchmark
|
|
314
|
+
"limitBy": "iterations", // Limit mode: 'iterations', 'time', 'any', 'all'
|
|
315
|
+
"outputDir": "./benchmark-results", // Directory for results and reports
|
|
316
|
+
"pattern": "benchmarks/**/*.bench.{js,ts}", // Glob pattern to discover benchmark files
|
|
317
|
+
"quiet": false, // Minimal output mode
|
|
318
|
+
"reporters": ["human", "json"], // Output reporters to use
|
|
319
|
+
"tags": ["fast", "critical"], // Tags to include (if empty, all benchmarks run)
|
|
320
|
+
"time": 5000, // Time budget in ms per benchmark
|
|
321
|
+
"timeout": 30000, // Task timeout in ms
|
|
322
|
+
"verbose": false, // Detailed output with debugging info
|
|
323
|
+
"warmup": 50, // Warmup iterations before measurement
|
|
324
|
+
}
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Configuration Options:**
|
|
328
|
+
|
|
329
|
+
- `pattern` - Glob pattern(s) to discover benchmark files (can be string or array)
|
|
330
|
+
- `exclude` - Glob patterns for files/directories to exclude from discovery
|
|
331
|
+
- `excludeTags` - Array of tags to exclude from execution; benchmarks with ANY of these tags will be skipped (default: [])
|
|
332
|
+
- `iterations` - Number of samples to collect per benchmark task (default: 100)
|
|
333
|
+
- `time` - Time budget in milliseconds per benchmark task (default: 1000)
|
|
334
|
+
- `limitBy` - How to limit benchmarks: `"iterations"` (sample count), `"time"` (time budget), `"any"` (whichever comes first), or `"all"` (both thresholds required)
|
|
335
|
+
- `warmup` - Number of warmup iterations before measurement begins (default: 0)
|
|
336
|
+
- `timeout` - Maximum time in milliseconds for a single task before timing out (default: 30000)
|
|
337
|
+
- `bail` - Stop execution on first benchmark failure (default: false)
|
|
338
|
+
- `reporters` - Array of reporter names to use for output (available: `"human"`, `"json"`, `"csv"`)
|
|
339
|
+
- `outputDir` - Directory path for saving benchmark results and reports
|
|
340
|
+
- `quiet` - Minimal output mode, suppresses non-essential messages (default: false)
|
|
341
|
+
- `tags` - Array of tags to include; if non-empty, only benchmarks with ANY of these tags will run (default: [])
|
|
342
|
+
- `verbose` - Detailed output mode with additional debugging information (default: false)
|
|
343
|
+
|
|
344
|
+
> **Note:** Smart defaults apply for `limitBy` based on which options you provide. See [Controlling Benchmark Limits](#controlling-benchmark-limits) for details.
|
|
345
|
+
|
|
346
|
+
### Configuration File Support
|
|
347
|
+
|
|
348
|
+
**modestbench** supports multiple configuration file formats, powered by [cosmiconfig](https://github.com/cosmiconfig/cosmiconfig):
|
|
349
|
+
|
|
350
|
+
- **JSON**: `modestbench.config.json`, `.modestbenchrc.json`, `.modestbenchrc`
|
|
351
|
+
- **YAML**: `modestbench.config.yaml`, `modestbench.config.yml`, `.modestbenchrc.yaml`, `.modestbenchrc.yml`
|
|
352
|
+
- **JavaScript**: `modestbench.config.js`, `modestbench.config.mjs`, `.modestbenchrc.js`, `.modestbenchrc.mjs`
|
|
353
|
+
- **TypeScript**: `modestbench.config.ts`
|
|
354
|
+
- **package.json**: Use a `"modestbench"` field
|
|
355
|
+
|
|
356
|
+
Generate a configuration file using:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
modestbench init --config-type json # JSON format
|
|
360
|
+
modestbench init --config-type yaml # YAML format
|
|
361
|
+
modestbench init --config-type js # JavaScript format
|
|
362
|
+
modestbench init --config-type ts # TypeScript format
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**Configuration Discovery**: **modestbench** automatically searches for configuration files in the current directory and parent directories, following standard conventions.
|
|
366
|
+
|
|
367
|
+
## Output Formats
|
|
368
|
+
|
|
369
|
+
### Human-Readable (Default)
|
|
370
|
+
|
|
371
|
+
Real-time progress bars with color-coded results and performance summaries.
|
|
372
|
+
|
|
373
|
+
### JSON Output
|
|
374
|
+
|
|
375
|
+
Structured data perfect for programmatic analysis and integration:
|
|
376
|
+
|
|
377
|
+
```json
|
|
378
|
+
{
|
|
379
|
+
"results": [
|
|
380
|
+
{
|
|
381
|
+
"file": "example.bench.js",
|
|
382
|
+
"suite": "Array Operations",
|
|
383
|
+
"task": "Array.push()",
|
|
384
|
+
"hz": 1234567.89,
|
|
385
|
+
"stats": {
|
|
386
|
+
"mean": 0.00081,
|
|
387
|
+
"stdDev": 0.00002,
|
|
388
|
+
"marginOfError": 2.45
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
],
|
|
392
|
+
"run": {
|
|
393
|
+
"id": "run-2025-10-07-001",
|
|
394
|
+
"timestamp": "2025-10-07T10:30:00.000Z",
|
|
395
|
+
"duration": 15420,
|
|
396
|
+
"status": "completed"
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### CSV Export
|
|
402
|
+
|
|
403
|
+
Tabular data for spreadsheet analysis and historical tracking.
|
|
404
|
+
|
|
405
|
+
## Advanced Features
|
|
406
|
+
|
|
407
|
+
### Multiple Suites
|
|
408
|
+
|
|
409
|
+
```javascript
|
|
410
|
+
const state = {
|
|
411
|
+
data: [],
|
|
412
|
+
sortedData: [],
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
export default {
|
|
416
|
+
suites: {
|
|
417
|
+
Sorting: {
|
|
418
|
+
setup() {
|
|
419
|
+
state.data = generateTestData(1000);
|
|
420
|
+
},
|
|
421
|
+
benchmarks: {
|
|
422
|
+
// Shorthand syntax for simple benchmarks
|
|
423
|
+
'Quick Sort': () => quickSort(state.data),
|
|
424
|
+
'Merge Sort': () => mergeSort(state.data),
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
Searching: {
|
|
429
|
+
setup() {
|
|
430
|
+
state.sortedData = generateSortedData(10000);
|
|
431
|
+
},
|
|
432
|
+
benchmarks: {
|
|
433
|
+
'Binary Search': () => binarySearch(state.sortedData, 5000),
|
|
434
|
+
'Linear Search': () => linearSearch(state.sortedData, 5000),
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
};
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Async Operations
|
|
442
|
+
|
|
443
|
+
```javascript
|
|
444
|
+
export default {
|
|
445
|
+
suites: {
|
|
446
|
+
'Async Performance': {
|
|
447
|
+
benchmarks: {
|
|
448
|
+
// Shorthand syntax works with async functions too
|
|
449
|
+
'Promise.resolve()': async () => {
|
|
450
|
+
return await Promise.resolve('test');
|
|
451
|
+
},
|
|
452
|
+
|
|
453
|
+
// Full syntax when you need config, tags, or metadata
|
|
454
|
+
'Fetch Simulation': {
|
|
455
|
+
async fn() {
|
|
456
|
+
const response = await simulateApiCall();
|
|
457
|
+
return response.json();
|
|
458
|
+
},
|
|
459
|
+
config: {
|
|
460
|
+
iterations: 100, // Fewer iterations for slow operations
|
|
461
|
+
},
|
|
462
|
+
},
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Tagging and Filtering
|
|
470
|
+
|
|
471
|
+
**modestbench** supports a powerful tagging system that lets you organize and selectively run benchmarks. Tags can be applied at three levels: file, suite, and task. Tags automatically cascade from parent to child, so tasks inherit tags from their suite and file.
|
|
472
|
+
|
|
473
|
+
#### Adding Tags
|
|
474
|
+
|
|
475
|
+
Tags can be added at any level:
|
|
476
|
+
|
|
477
|
+
```javascript
|
|
478
|
+
export default {
|
|
479
|
+
// File-level tags (inherited by all suites and tasks)
|
|
480
|
+
tags: ['performance', 'core'],
|
|
481
|
+
|
|
482
|
+
suites: {
|
|
483
|
+
'String Operations': {
|
|
484
|
+
// Suite-level tags (inherited by all tasks in this suite)
|
|
485
|
+
tags: ['string', 'fast'],
|
|
486
|
+
|
|
487
|
+
benchmarks: {
|
|
488
|
+
// Task inherits: ['performance', 'core', 'string', 'fast', 'regex']
|
|
489
|
+
'RegExp Test': {
|
|
490
|
+
fn: () => /pattern/.test(str),
|
|
491
|
+
tags: ['regex'], // Task-specific tags
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
// Task inherits: ['performance', 'core', 'string', 'fast']
|
|
495
|
+
'String Includes': () => str.includes('pattern'),
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
#### Filtering Benchmarks
|
|
503
|
+
|
|
504
|
+
Use `--tags` to include only benchmarks with specific tags (OR logic - matches ANY tag):
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
# Run only fast algorithms
|
|
508
|
+
modestbench run --tags fast
|
|
509
|
+
|
|
510
|
+
# Run benchmarks tagged with 'string' OR 'array'
|
|
511
|
+
modestbench run --tags string,array
|
|
512
|
+
|
|
513
|
+
# Multiple tags can be space-separated too
|
|
514
|
+
modestbench run --tags fast optimized
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
Use `--exclude-tags` to skip benchmarks with specific tags:
|
|
518
|
+
|
|
519
|
+
```bash
|
|
520
|
+
# Exclude slow benchmarks
|
|
521
|
+
modestbench run --exclude-tags slow
|
|
522
|
+
|
|
523
|
+
# Exclude experimental and unstable benchmarks
|
|
524
|
+
modestbench run --exclude-tags experimental,unstable
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Combine both to fine-tune your benchmark runs (exclusion takes precedence):
|
|
528
|
+
|
|
529
|
+
```bash
|
|
530
|
+
# Run fast benchmarks, but exclude experimental ones
|
|
531
|
+
modestbench run --tags fast --exclude-tags experimental
|
|
532
|
+
|
|
533
|
+
# Run algorithm benchmarks but skip slow reference implementations
|
|
534
|
+
modestbench run --tags algorithm --exclude-tags slow,reference
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
#### Tag Cascading Example
|
|
538
|
+
|
|
539
|
+
```javascript
|
|
540
|
+
export default {
|
|
541
|
+
tags: ['file-level'], // All tasks get this tag
|
|
542
|
+
|
|
543
|
+
suites: {
|
|
544
|
+
'Fast Suite': {
|
|
545
|
+
tags: ['fast'], // Tasks get: ['file-level', 'fast']
|
|
546
|
+
benchmarks: {
|
|
547
|
+
'Task A': {
|
|
548
|
+
fn: () => {},
|
|
549
|
+
tags: ['math'], // This task has: ['file-level', 'fast', 'math']
|
|
550
|
+
},
|
|
551
|
+
'Task B': () => {}, // This task has: ['file-level', 'fast']
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**Filtering Behavior:**
|
|
559
|
+
|
|
560
|
+
- `--tags math` → Runs only Task A (matches 'math')
|
|
561
|
+
- `--tags fast` → Runs both Task A and Task B (both have 'fast')
|
|
562
|
+
- `--tags file-level` → Runs both tasks (both inherit 'file-level')
|
|
563
|
+
- `--exclude-tags math` → Runs only Task B (Task A excluded)
|
|
564
|
+
|
|
565
|
+
#### Suite Lifecycle with Filtering
|
|
566
|
+
|
|
567
|
+
Suite `setup()` and `teardown()` only run if at least one task in the suite matches the filter. This prevents unnecessary setup work for filtered-out suites.
|
|
568
|
+
|
|
569
|
+
## Integration Examples
|
|
570
|
+
|
|
571
|
+
### GitHub Actions
|
|
572
|
+
|
|
573
|
+
```yaml
|
|
574
|
+
name: Performance Tests
|
|
575
|
+
on: [push, pull_request]
|
|
576
|
+
|
|
577
|
+
jobs:
|
|
578
|
+
benchmark:
|
|
579
|
+
runs-on: ubuntu-latest
|
|
580
|
+
steps:
|
|
581
|
+
- uses: actions/checkout@v3
|
|
582
|
+
- uses: actions/setup-node@v3
|
|
583
|
+
with:
|
|
584
|
+
node-version: 18
|
|
585
|
+
|
|
586
|
+
- run: npm ci
|
|
587
|
+
- run: npm run build
|
|
588
|
+
|
|
589
|
+
- name: Run Benchmarks
|
|
590
|
+
run: |
|
|
591
|
+
modestbench run \
|
|
592
|
+
--reporters json,csv \
|
|
593
|
+
--output ./results
|
|
594
|
+
|
|
595
|
+
- name: Upload Results
|
|
596
|
+
uses: actions/upload-artifact@v3
|
|
597
|
+
with:
|
|
598
|
+
name: benchmark-results
|
|
599
|
+
path: ./results/
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### Performance Regression Detection
|
|
603
|
+
|
|
604
|
+
```javascript
|
|
605
|
+
// scripts/check-regression.js
|
|
606
|
+
import { execSync } from 'child_process';
|
|
607
|
+
import { readFileSync } from 'fs';
|
|
608
|
+
|
|
609
|
+
// Run current benchmarks
|
|
610
|
+
execSync('modestbench run --reporters json --output ./current');
|
|
611
|
+
const current = JSON.parse(readFileSync('./current/results.json'));
|
|
612
|
+
|
|
613
|
+
// Load baseline results
|
|
614
|
+
const baseline = JSON.parse(readFileSync('./baseline/results.json'));
|
|
615
|
+
|
|
616
|
+
// Check for significant regressions
|
|
617
|
+
for (const result of current.results) {
|
|
618
|
+
const baselineResult = baseline.results.find(
|
|
619
|
+
(r) => r.file === result.file && r.task === result.task,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
if (baselineResult) {
|
|
623
|
+
const regression = (baselineResult.hz - result.hz) / baselineResult.hz;
|
|
624
|
+
if (regression > 0.1) {
|
|
625
|
+
// 10% regression threshold
|
|
626
|
+
console.error(
|
|
627
|
+
`Performance regression detected in ${result.task}: ${(regression * 100).toFixed(1)}%`,
|
|
628
|
+
);
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
console.log('No performance regressions detected ✅');
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
## Programmatic API
|
|
638
|
+
|
|
639
|
+
```typescript
|
|
640
|
+
import { modestbench, HumanReporter } from 'modestbench';
|
|
641
|
+
|
|
642
|
+
// initialize the engine
|
|
643
|
+
const engine = modestbench();
|
|
644
|
+
|
|
645
|
+
engine.registerReporter('human', new HumanReporter());
|
|
646
|
+
|
|
647
|
+
// Execute benchmarks
|
|
648
|
+
const result = await engine.execute({
|
|
649
|
+
pattern: '**/*.bench.js',
|
|
650
|
+
iterations: 1000,
|
|
651
|
+
});
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
## Contributing
|
|
655
|
+
|
|
656
|
+
We welcome contributions! Please see our [Contributing Guide][contributing] for details.
|
|
657
|
+
|
|
658
|
+
### Development Setup
|
|
659
|
+
|
|
660
|
+
```bash
|
|
661
|
+
# Clone the repository
|
|
662
|
+
git clone https://github.com/boneskull/modestbench.git
|
|
663
|
+
cd modestbench
|
|
664
|
+
|
|
665
|
+
# Install dependencies
|
|
666
|
+
npm install
|
|
667
|
+
|
|
668
|
+
# Run tests
|
|
669
|
+
npm test
|
|
670
|
+
|
|
671
|
+
# Build the project
|
|
672
|
+
npm run build
|
|
673
|
+
|
|
674
|
+
# Run examples
|
|
675
|
+
npm run examples
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
## Acknowledgments
|
|
679
|
+
|
|
680
|
+
- Built on top of the small-but-mighty benchmarking library, [tinybench][]
|
|
681
|
+
- Interface inspired by good ol' [Benchmark.js][]
|
|
682
|
+
- Built with [zshy][] for dual ESM/CJS modules
|
|
683
|
+
|
|
684
|
+
## Resources
|
|
685
|
+
|
|
686
|
+
- [Issue Tracker][bugs]
|
|
687
|
+
- [Discussions][]
|
|
688
|
+
|
|
689
|
+
## License
|
|
690
|
+
|
|
691
|
+
Copyright © 2025 [Christopher Hiller](https://github.com/boneskull). Licensed under the [Blue Oak Model License 1.0.0][license].
|
|
692
|
+
|
|
693
|
+
[license]: https://blueoakcouncil.org/license/1.0.0
|
|
694
|
+
[tinybench]: https://github.com/tinylibs/tinybench
|
|
695
|
+
[benchmark.js]: https://benchmarkjs.com/
|
|
696
|
+
[bugs]: https://github.com/boneskull/modestbench/issues
|
|
697
|
+
[discussions]: https://github.com/boneskull/modestbench/discussions
|
|
698
|
+
[zshy]: https://github.com/colinhacks/zshy
|
|
699
|
+
[contributing]: CONTRIBUTING.md
|