Skip to content

Commit

Permalink
refactor: factor out main code from exported files
Browse files Browse the repository at this point in the history
Signed-off-by: Jérôme Benoit <[email protected]>
  • Loading branch information
jerome-benoit committed Apr 15, 2024
1 parent 07069b9 commit c65b496
Show file tree
Hide file tree
Showing 5 changed files with 294 additions and 313 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ and this project adheres to

## [Unreleased]

### Fixed

- Fix ESM browser export.

### Changed

- BREAKING CHANGE: rename the default exported file to a more de facto
standardized namespace. Usage in browser is impacted.

## [0.3.4] - 2024-04-14

### Added
Expand Down
276 changes: 276 additions & 0 deletions src/benchmark.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
import {
defaultSamples,
defaultTime,
emptyFunction,
tatamiNgGroup,
} from './constants.mjs';
import {
os,
AsyncFunction,
checkBenchmarkArgs,
cpu,
measure,
mergeDeepRight,
noColor,
version,
} from './lib.mjs';
import { logger } from './logger.mjs';
import * as clr from './reporter/clr.mjs';
import * as table from './reporter/table.mjs';
import { runtime } from './runtime.mjs';

let groupName = null;
const groups = new Map();
const benchmarks = [];

export function group(name, cb) {
if (
name != null &&
'string' !== typeof name &&
Object.prototype.toString.call(name).slice(8, -1) !== 'Object' &&
![Function, AsyncFunction].includes(name.constructor)
)
throw new TypeError(
`expected string, object or function, got ${name.constructor.name}`,
);
if ([Function, AsyncFunction].includes(name.constructor)) {
// biome-ignore lint/style/noParameterAssign: <explanation>
cb = name;
}
if (cb != null && ![Function, AsyncFunction].includes(cb.constructor))
throw new TypeError(`expected function, got ${cb.constructor.name}`);
if (Object.prototype.toString.call(name).slice(8, -1) === 'Object') {
if (name.name != null && 'string' !== typeof name.name)
throw new TypeError(
`expected string as 'name' option, got ${name.name.constructor.name}`,
);
if (name.summary != null && 'boolean' !== typeof name.summary)
throw new TypeError(
`expected boolean as 'summary' option, got ${name.summary.constructor.name}`,
);
if (
name.before != null &&
![Function, AsyncFunction].includes(name.before.constructor)
)
throw new TypeError(
`expected function as 'before' option, got ${name.before.constructor.name}`,
);
if (
name.after != null &&
![Function, AsyncFunction].includes(name.after.constructor)
)
throw new TypeError(
`expected function as 'after' option, got ${name.after.constructor.name}`,
);
}

groupName =
('string' === typeof name ? name.trim() : name.name?.trim()) ||
`${tatamiNgGroup}${groups.size + 1}`;
if (!groups.has(groupName))
groups.set(groupName, {
summary: name.summary ?? true,
before: name.before ?? emptyFunction,
after: name.after ?? emptyFunction,
});
cb();
groupName = null;
}

export function bench(name, fn, opts = {}) {
if ([Function, AsyncFunction].includes(name.constructor)) {
// biome-ignore lint/style/noParameterAssign: <explanation>
fn = name;
// biome-ignore lint/style/noParameterAssign: <explanation>
name = fn.name;
}
checkBenchmarkArgs(fn, opts);
// biome-ignore lint/style/noParameterAssign: <explanation>
name = name.trim();

benchmarks.push({
before: opts.before ?? emptyFunction,
fn,
after: opts.after ?? emptyFunction,
name,
group: groupName,
time: defaultTime,
warmup: opts.warmup ?? true,
samples: defaultSamples,
baseline: false,
async: AsyncFunction === fn.constructor,
});
}

export function baseline(name, fn, opts = {}) {
if ([Function, AsyncFunction].includes(name.constructor)) {
// biome-ignore lint/style/noParameterAssign: <explanation>
fn = name;
// biome-ignore lint/style/noParameterAssign: <explanation>
name = fn.name;
}
checkBenchmarkArgs(fn, opts);
// biome-ignore lint/style/noParameterAssign: <explanation>
name = name.trim();

benchmarks.push({
before: opts.before ?? emptyFunction,
fn,
after: opts.after ?? emptyFunction,
name,
group: groupName,
time: defaultTime,
warmup: opts.warmup ?? true,
samples: defaultSamples,
baseline: true,
async: AsyncFunction === fn.constructor,
});
}

export function clear() {
groups.clear();
benchmarks.length = 0;
}

export async function run(opts = {}) {
if (Object.prototype.toString.call(opts).slice(8, -1) !== 'Object')
throw new TypeError(`expected object, got ${opts.constructor.name}`);
if (opts.samples != null && 'number' !== typeof opts.samples)
throw new TypeError(
`expected number as 'samples' option, got ${opts.samples.constructor.name}`,
);
if (opts.time != null && 'number' !== typeof opts.time)
throw new TypeError(
`expected number as 'time' option, got ${opts.time.constructor.name}`,
);
if (
opts.json != null &&
'number' !== typeof opts.json &&
'boolean' !== typeof opts.json
)
throw new TypeError(
`expected number or boolean as 'json' option, got ${opts.json.constructor.name}`,
);
// biome-ignore lint/style/noParameterAssign: <explanation>
opts = mergeDeepRight(
{
silent: false,
colors: !noColor,
size: table.size(benchmarks.map(benchmark => benchmark.name)),
},
opts,
);

const log = opts.silent === true ? emptyFunction : logger;

const report = {
benchmarks,
cpu,
runtime: `${runtime} ${version} (${os})`,
};

if (!opts.json && benchmarks.length > 0) {
log(clr.gray(opts.colors, `cpu: ${report.cpu}`));
log(clr.gray(opts.colors, `runtime: ${report.runtime}`));

log('');
log(table.header(opts));
log(table.br(opts));
}

let _baseline = false;
let _first = false;
for (const benchmark of benchmarks) {
if (benchmark.group) continue;
if (benchmark.baseline) _baseline = true;

benchmark.samples = opts.samples ?? benchmark.samples;
benchmark.time = opts.time ?? benchmark.time;
_first = true;
try {
benchmark.stats = (
await measure(benchmark.fn, benchmark.before, benchmark.after, {
async: benchmark.async,
warmup: benchmark.warmup,
samples: benchmark.samples,
time: benchmark.time,
})
).stats;
if (!opts.json)
log(table.benchmark(benchmark.name, benchmark.stats, opts));
} catch (err) {
benchmark.error = err;
if (!opts.json)
log(table.benchmarkError(benchmark.name, benchmark.error, opts));
}
}

if (_baseline && !opts.json)
log(
`\n${table.summary(
benchmarks.filter(benchmark => benchmark.group == null),
opts,
)}`,
);

for (const [group, groupOpts] of groups) {
if (!opts.json) {
if (_first) log('');
if (!group.startsWith(tatamiNgGroup)) log(`• ${group}`);
if (_first || !group.startsWith(tatamiNgGroup))
log(clr.gray(opts.colors, table.br(opts)));
}

AsyncFunction === groupOpts.before.constructor
? await groupOpts.before()
: groupOpts.before();

for (const benchmark of benchmarks) {
if (group !== benchmark.group) continue;

benchmark.samples = opts.samples ?? benchmark.samples;
benchmark.time = opts.time ?? benchmark.time;
_first = true;
try {
benchmark.stats = (
await measure(benchmark.fn, benchmark.before, benchmark.after, {
async: benchmark.async,
warmup: benchmark.warmup,
samples: benchmark.samples,
time: benchmark.time,
})
).stats;
if (!opts.json)
log(table.benchmark(benchmark.name, benchmark.stats, opts));
} catch (err) {
benchmark.error = err;
if (!opts.json)
log(table.benchmarkError(benchmark.name, benchmark.error, opts));
}
}

AsyncFunction === groupOpts.after.constructor
? await groupOpts.after()
: groupOpts.after();

if (groupOpts.summary === true && !opts.json)
log(
`\n${table.summary(
benchmarks.filter(benchmark => group === benchmark.group),
opts,
)}`,
);
}

if (!opts.json && opts.units) log(table.units(opts));
if (opts.json)
log(
JSON.stringify(
report,
undefined,
'number' !== typeof opts.json ? 0 : opts.json,
),
);

return report;
}
1 change: 1 addition & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function group(
},
cb: () => void,
): void;

export function bench(
name: string,
fn: () => void | Promise<void>,
Expand Down
Loading

0 comments on commit c65b496

Please sign in to comment.