diff --git a/.vscode/settings.json b/.vscode/settings.json
index 86b29b6..d409450 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -23,6 +23,7 @@
"npmjs",
"outdir",
"Parens",
+ "peowly",
"poolifier",
"quantile",
"quantiles",
diff --git a/README.md b/README.md
index 390770d..9b46606 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
tatami-ng
-Cross JavaScript runtime benchmarking library
+Cross JavaScript runtime benchmarking library and CLI
@@ -12,6 +12,7 @@
+- CLI and JS library support ✔
- Library API backward compatible with [mitata](https://github.com/evanwashere/mitata) ✔
- Benchmark latency and throughput ✔
- Support for sync and async benchmark ✔
@@ -20,7 +21,7 @@
- Zero cost abstraction for multiple JS runtime support ✔
- Support for ESM and TypeScript ✔
-## Install
+## Library installation
### Node
@@ -44,8 +45,7 @@ deno add @poolifier/tatami-ng
Deno versions >= 1.40.x are supported.
-The `--allow-hrtime` permission flag is recommended to allow high-resolution
-time measurement.
+The `--allow-hrtime` permission flag is recommended to allow high-resolution time measurement.
### Bun
@@ -77,7 +77,7 @@ import {
-## Example
+## Library usage example
```js
// adapt import to the targeted JS runtime
@@ -124,12 +124,36 @@ await run({
clear();
```
+## CLI installation
+
+### Node
+
+```shell
+npm install tatami-ng -g
+```
+
+### Bun
+
+```shell
+bun add tatami-ng -g
+```
+
+### Deno
+
+```shell
+deno install --allow-sys --allow-read --allow-hrtime --name tatami https://esm.sh/jsr/@poolifier/tatami-ng/cli.js
+```
+
+## CLI usage examples
+
+```shell
+tatami 'hexdump ' 'xxd '
+```
+
## Development
-The JavaScript runtime environment used for development is
-[bun](https://bun.sh/).
+The JavaScript runtime environment used for development is [bun](https://bun.sh/).
## License
-MIT © [Evan](https://github.com/evanwashere),
-[Jerome Benoit](https://github.com/jerome-benoit)
+MIT © [Evan](https://github.com/evanwashere), [Jerome Benoit](https://github.com/jerome-benoit)
diff --git a/bun.lockb b/bun.lockb
index eeaa526..e349e57 100755
Binary files a/bun.lockb and b/bun.lockb differ
diff --git a/cli.js b/cli.js
new file mode 100755
index 0000000..133dccc
--- /dev/null
+++ b/cli.js
@@ -0,0 +1,145 @@
+#!/usr/bin/env node
+
+import { readFileSync } from 'node:fs'
+import { peowly } from 'peowly'
+
+import {
+ baseline as baselineBenchmark,
+ bench as benchmark,
+ run,
+} from './src/index.js'
+import { spawnSync } from './src/lib.js'
+
+const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url)))
+
+const {
+ flags: { baseline, bench, ...flags },
+} = peowly({
+ options: {
+ baseline: {
+ listGroup: 'Benchmark options',
+ description: 'Baseline command',
+ type: 'string',
+ short: 'B',
+ },
+ bench: {
+ listGroup: 'Benchmark options',
+ description: 'Benchmark command',
+ type: 'string',
+ multiple: true,
+ short: 'b',
+ },
+ samples: {
+ listGroup: 'Benchmark options',
+ description: 'Minimum number of samples',
+ type: 'string',
+ short: 's',
+ },
+ time: {
+ listGroup: 'Benchmark options',
+ description: 'Minimum time in nanoseconds',
+ type: 'string',
+ short: 't',
+ },
+ 'no-warmup': {
+ listGroup: 'Benchmark options',
+ description: 'No warmup',
+ type: 'boolean',
+ },
+ silent: {
+ listGroup: 'Output options',
+ description: 'No stdout output',
+ type: 'boolean',
+ },
+ json: {
+ listGroup: 'Output options',
+ description: 'Outputs as JSON',
+ type: 'string',
+ short: 'j',
+ },
+ file: {
+ listGroup: 'Output options',
+ description: 'Outputs as JSON to a file',
+ type: 'string',
+ short: 'f',
+ },
+ 'no-colors': {
+ listGroup: 'Output options',
+ description: 'No colors in output',
+ type: 'boolean',
+ },
+ 'no-avg': {
+ listGroup: 'Output options',
+ description: 'No average column',
+ type: 'boolean',
+ },
+ 'no-iter': {
+ listGroup: 'Output options',
+ description: 'No iterations per second column',
+ type: 'boolean',
+ },
+ 'no-min_max': {
+ listGroup: 'Output options',
+ description: 'No (min...max) column',
+ type: 'boolean',
+ },
+ 'no-rmoe': {
+ listGroup: 'Output options',
+ description: 'No error margin column',
+ type: 'boolean',
+ },
+ 'no-percentiles': {
+ listGroup: 'Output options',
+ description: 'No percentile columns',
+ type: 'boolean',
+ },
+ units: {
+ listGroup: 'Output options',
+ description: 'Print units cheatsheet',
+ type: 'boolean',
+ short: 'u',
+ },
+ },
+ description: 'tatami-ng CLI for running benchmark',
+ examples: ['--baseline --bench --bench '],
+ name: 'tatami',
+ pkg,
+})
+
+if (baseline != null) {
+ baselineBenchmark(baseline, () => {
+ spawnSync(baseline)
+ })
+}
+if (bench != null) {
+ for (const b of bench) {
+ benchmark(b, () => {
+ spawnSync(b)
+ })
+ }
+}
+
+if (flags.json != null) {
+ const json = Number.parseInt(flags.json)
+ if (!Number.isNaN(json)) {
+ flags.json = json
+ }
+}
+
+await run({
+ ...(flags.samples != null && { samples: Number.parseInt(flags.samples) }),
+ ...(flags.time != null && { time: Number.parseFloat(flags.time) }),
+ ...(flags['no-warmup'] != null && { warmup: !flags['no-warmup'] }),
+ ...(flags.silent != null && { silent: flags.silent }),
+ ...(flags.json != null && { json: flags.json }),
+ ...(flags.file != null && { file: flags.file }),
+ ...(flags['no-colors'] != null && { colors: !flags['no-colors'] }),
+ ...(flags['no-avg'] != null && { avg: !flags['no-avg'] }),
+ ...(flags['no-rmoe'] != null && { rmoe: !flags['no-rmoe'] }),
+ ...(flags['no-iter'] != null && { iter: !flags['no-iter'] }),
+ ...(flags['no-min_max'] != null && { min_max: !flags['no-min_max'] }),
+ ...(flags['no-percentiles'] != null && {
+ percentiles: !flags['no-percentiles'],
+ }),
+ ...(flags.units != null && { units: flags.units }),
+})
diff --git a/jsr.json b/jsr.json
index fb28eca..207e909 100644
--- a/jsr.json
+++ b/jsr.json
@@ -7,7 +7,9 @@
"include": [
"LICENSE",
"README.md",
+ "cli.js",
"jsr.json",
+ "package.json",
"src/**/*.js",
"src/**/*.d.ts"
]
diff --git a/package.json b/package.json
index 89d2a14..cebf073 100644
--- a/package.json
+++ b/package.json
@@ -5,6 +5,9 @@
"license": "MIT",
"version": "0.4.16",
"packageManager": "bun@1.1.18",
+ "bin": {
+ "tatami": "cli.js"
+ },
"types": "./src/index.d.ts",
"main": "./src/index.js",
"browser": "./dist/browser/index.js",
@@ -25,17 +28,20 @@
},
"files": ["src", "dist"],
"scripts": {
- "prepare": "bun run prepare.ts",
+ "prepare": "bun prepare.ts",
"format": "biome format . --write",
"lint": "biome lint .",
"lint:write": "biome lint . --write",
"check": "biome check .",
"check:write": "biome check . --write",
"check:ci": "biome ci .",
- "bundle": "bun run bundle.ts",
+ "bundle": "bun bundle.ts",
"test:node": "node tests/test.js",
"test:deno": "deno run -A tests/test.js && deno run -A tests/test.ts",
- "test:bun": "bun run tests/test.js && bun run tests/test.ts"
+ "test:bun": "bun tests/test.js && bun tests/test.ts"
+ },
+ "dependencies": {
+ "peowly": "^1.3.2"
},
"devDependencies": {
"@biomejs/biome": "^1.8.3",
diff --git a/src/benchmark.js b/src/benchmark.js
index 43fc1b9..b27d572 100644
--- a/src/benchmark.js
+++ b/src/benchmark.js
@@ -193,7 +193,12 @@ export function clear() {
benchmarks.length = 0
}
-const executeBenchmarks = async (benchmarks, log, opts = {}) => {
+const executeBenchmarks = async (
+ benchmarks,
+ logFn,
+ opts = {},
+ groupOpts = {}
+) => {
let once = false
for (const benchmark of benchmarks) {
once = true
@@ -211,13 +216,23 @@ const executeBenchmarks = async (benchmarks, log, opts = {}) => {
}
)
if (!opts.json)
- log(table.benchmark(benchmark.name, benchmark.stats, opts))
+ logFn(table.benchmark(benchmark.name, benchmark.stats, opts))
} catch (err) {
benchmark.error = err
if (!opts.json)
- log(table.benchmarkError(benchmark.name, benchmark.error, opts))
+ logFn(table.benchmarkError(benchmark.name, benchmark.error, opts))
}
}
+ // biome-ignore lint/style/noParameterAssign:
+ benchmarks = benchmarks.filter(benchmark => benchmark.error == null)
+ if (
+ (Object.keys(groupOpts).length === 0 || groupOpts.summary === true) &&
+ !opts.json &&
+ benchmarks.length > 1
+ ) {
+ logFn('')
+ logFn(table.summary(benchmarks, opts))
+ }
return once
}
@@ -306,18 +321,11 @@ export async function run(opts = {}) {
log(table.br(opts))
}
- let noGroupBenchmarks = benchmarks.filter(
- benchmark => benchmark.group == null
- )
- let once = await executeBenchmarks(noGroupBenchmarks, log, opts)
-
- noGroupBenchmarks = noGroupBenchmarks.filter(
- noGroupBenchmark => noGroupBenchmark.error == null
+ let once = await executeBenchmarks(
+ benchmarks.filter(benchmark => benchmark.group == null),
+ log,
+ opts
)
- if (!opts.json && noGroupBenchmarks.length > 1) {
- log('')
- log(table.summary(noGroupBenchmarks, opts))
- }
for (const [group, groupOpts] of groups) {
if (!opts.json) {
@@ -327,31 +335,20 @@ export async function run(opts = {}) {
log(clr.gray(opts.colors, table.br(opts)))
}
- let groupBenchmarks = benchmarks.filter(
- benchmark => benchmark.group === group
- )
-
AsyncFunction === groupOpts.before.constructor
? await groupOpts.before()
: groupOpts.before()
- once = await executeBenchmarks(groupBenchmarks, log, opts)
+ once = await executeBenchmarks(
+ benchmarks.filter(benchmark => benchmark.group === group),
+ log,
+ opts,
+ groupOpts
+ )
AsyncFunction === groupOpts.after.constructor
? await groupOpts.after()
: groupOpts.after()
-
- groupBenchmarks = groupBenchmarks.filter(
- groupBenchmark => groupBenchmark.error == null
- )
- if (
- groupOpts.summary === true &&
- !opts.json &&
- groupBenchmarks.length > 1
- ) {
- log('')
- log(table.summary(groupBenchmarks, opts))
- }
}
if (!opts.json && opts.units) log(table.units(opts))
diff --git a/src/lib.js b/src/lib.js
index 0a2a2f7..c0e68ad 100644
--- a/src/lib.js
+++ b/src/lib.js
@@ -8,6 +8,8 @@ import {
import { runtime } from './runtime.js'
import { now } from './time.js'
+import { spawnSync as nodeSpawnSync } from 'node:child_process'
+
export const AsyncFunction = (async () => {}).constructor
export const version = (() => {
@@ -60,6 +62,22 @@ export const writeFileSync = await (async () => {
}[runtime]()
})()
+export const spawnSync = await (async () => {
+ return await {
+ unknown: () => () => {},
+ browser: () => () => {},
+ node: () => command =>
+ nodeSpawnSync(command.split(' ')[0], command.split(' ').slice(1)),
+ deno: () => command => {
+ const cmd = new Deno.Command(command.split(' ')[0], {
+ args: command.split(' ').slice(1),
+ })
+ cmd.outputSync()
+ },
+ bun: () => command => Bun.spawnSync(command.split(' ')),
+ }[runtime]()
+})()
+
export const convertReportToBmf = report => {
return report.benchmarks
.map(({ name, stats }) => {