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 }) => {