Skip to content

Commit

Permalink
feat!: add CLI (#8)
Browse files Browse the repository at this point in the history
* feat: add CLI

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>

* fix: add missing cli.mjs file

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>

* feat: implement node CLI support

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>

* feat: add portable spawnSync implementation

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>

* fix: fix cli.js permissions

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>

* docs: document CLI installation

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>

* refactor: package.json scripts cleanup

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>

---------

Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
  • Loading branch information
jerome-benoit authored Jul 9, 2024
1 parent 394b033 commit d20b094
Show file tree
Hide file tree
Showing 8 changed files with 236 additions and 43 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"npmjs",
"outdir",
"Parens",
"peowly",
"poolifier",
"quantile",
"quantiles",
Expand Down
42 changes: 33 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<h1 align=center>tatami-ng</h1>

<h2 align=center>Cross JavaScript runtime benchmarking library</h2>
<h2 align=center>Cross JavaScript runtime benchmarking library and CLI</h2>

<div align="center">

Expand All @@ -12,6 +12,7 @@

</div>

- 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 ✔
Expand All @@ -20,7 +21,7 @@
- Zero cost abstraction for multiple JS runtime support ✔
- Support for ESM and TypeScript ✔

## Install
## Library installation

### Node

Expand All @@ -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

Expand Down Expand Up @@ -77,7 +77,7 @@ import {

<!-- x-release-please-end -->

## Example
## Library usage example

```js
// adapt import to the targeted JS runtime
Expand Down Expand Up @@ -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 <file>' 'xxd <file>'
```

## 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)
Binary file modified bun.lockb
Binary file not shown.
145 changes: 145 additions & 0 deletions cli.js
Original file line number Diff line number Diff line change
@@ -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 <command> --bench <command> --bench <command>'],
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 }),
})
2 changes: 2 additions & 0 deletions jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"include": [
"LICENSE",
"README.md",
"cli.js",
"jsr.json",
"package.json",
"src/**/*.js",
"src/**/*.d.ts"
]
Expand Down
12 changes: 9 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
59 changes: 28 additions & 31 deletions src/benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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: <explanation>
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
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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))
Expand Down
Loading

0 comments on commit d20b094

Please sign in to comment.