Skip to content

Commit

Permalink
refactor: major restructuration to ease pluggable features
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 Oct 2, 2024
1 parent 406e14b commit 25ac409
Show file tree
Hide file tree
Showing 12 changed files with 203 additions and 160 deletions.
50 changes: 29 additions & 21 deletions src/benchmark.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,31 @@ import {
AsyncFunction,
checkBenchmarkArgs,
colors,
convertReportToBmf,
cpuModel,
gc,
isObject,
measure,
overrideBenchmarkOptions,
version,
writeFileSync,
} from './lib.js'
import { logger } from './logger.js'
import * as clr from './reporter/clr.js'
import * as table from './reporter/table.js'
import { bmf } from './reporter/json/index.js'
import {
benchmarkError,
benchmarkReport,
bold,
br,
dim,
header,
size,
summary,
units,
warning,
white,
} from './reporter/terminal/index.js'
import { runtime } from './runtime.js'
import { now } from './time.js'
import { isObject } from './utils.js'

let groupName = null
const groups = new Map()
Expand Down Expand Up @@ -247,26 +258,26 @@ const executeBenchmarks = async (
after: benchmark.after,
})
if (!opts.json)
logFn(table.benchmark(benchmark.name, benchmark.stats, opts))
logFn(benchmarkReport(benchmark.name, benchmark.stats, opts))
} catch (err) {
benchmark.error = err
if (!opts.json)
logFn(table.benchmarkError(benchmark.name, benchmark.error, opts))
logFn(benchmarkError(benchmark.name, benchmark.error, opts))
}
}
// biome-ignore lint/style/noParameterAssign: <explanation>
benchmarks = benchmarks.filter(benchmark => benchmark.error == null)
if (!opts.json && table.warning(benchmarks, opts)) {
if (!opts.json && warning(benchmarks, opts)) {
logFn('')
logFn(table.warning(benchmarks, opts))
logFn(warning(benchmarks, opts))
}
if (
(Object.keys(groupOpts).length === 0 || groupOpts.summary === true) &&
!opts.json &&
benchmarks.length > 1
) {
logFn('')
logFn(table.summary(benchmarks, opts))
logFn(summary(benchmarks, opts))
}
return once
}
Expand Down Expand Up @@ -341,7 +352,7 @@ export async function run(opts = {}) {
opts.silent = opts.silent ?? false
opts.units = opts.units ?? false
opts.colors = opts.colors ?? colors
opts.size = table.size(benchmarks.map(benchmark => benchmark.name))
opts.size = size(benchmarks.map(benchmark => benchmark.name))

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

Expand All @@ -352,14 +363,12 @@ export async function run(opts = {}) {
}

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

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

let once = await executeBenchmarks(
Expand All @@ -371,10 +380,9 @@ export async function run(opts = {}) {
for (const [group, groupOpts] of groups) {
if (!opts.json) {
if (once) log('')
if (!group.startsWith(tatamiNgGroup))
log(`• ${clr.bold(opts.colors, group)}`)
if (!group.startsWith(tatamiNgGroup)) log(`• ${bold(opts.colors, group)}`)
if (once || !group.startsWith(tatamiNgGroup))
log(clr.dim(opts.colors, clr.white(opts.colors, table.br(opts))))
log(dim(opts.colors, white(opts.colors, br(opts))))
}

AsyncFunction === groupOpts.before.constructor
Expand All @@ -393,12 +401,12 @@ export async function run(opts = {}) {
: groupOpts.after()
}

if (!opts.json && opts.units) log(table.units(opts))
if (!opts.json && opts.units) log(units(opts))
if (opts.json || opts.file) {
let jsonReport
switch (opts.json) {
case jsonOutputFormat.bmf:
jsonReport = JSON.stringify(convertReportToBmf(report))
jsonReport = JSON.stringify(bmf(report))
break
default:
jsonReport = JSON.stringify(
Expand Down
6 changes: 4 additions & 2 deletions src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function run(options?: {
units?: boolean
}): Promise<Report>

export interface Report {
export type BenchmarkReport = {
cpu: string
runtime: string

Expand Down Expand Up @@ -97,5 +97,7 @@ export interface Report {
mad: number // median time absolute deviation
ss: boolean // statistical significance
}
}[]
}
}

export type Report = BenchmarkReport[]
3 changes: 2 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { group, bench, baseline, run } from './benchmark.js'
export { baseline, bench, group, run } from './benchmark.js'
export { bmf } from './reporter/json/index.js'
121 changes: 13 additions & 108 deletions src/lib.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { spawnSync as nodeSpawnSync } from 'node:child_process'
import { setFlagsFromString } from 'node:v8'
import { runInNewContext } from 'node:vm'
import {
defaultSamples,
defaultTime,
Expand All @@ -7,11 +10,15 @@ import {
tTable,
} from './constants.js'
import { runtime } from './runtime.js'
import {
absoluteDeviation,
average,
medianSorted,
quantileSorted,
variance,
} from './stats-utils.js'
import { now } from './time.js'

import { spawnSync as nodeSpawnSync } from 'node:child_process'
import { setFlagsFromString } from 'node:v8'
import { runInNewContext } from 'node:vm'
import { checkDividend, isObject } from './utils.js'

export const AsyncFunction = (async () => {}).constructor

Expand Down Expand Up @@ -133,28 +140,6 @@ export const gc = (() => {
}[runtime]()
})()

export const convertReportToBmf = report => {
return report.benchmarks
.map(({ name, stats }) => {
const throughputSd = ratioStandardDeviation(1e9, 0, stats.avg, stats.sd)
return {
[name]: {
latency: {
value: stats?.avg,
lower_value: stats?.avg - stats?.sd,
upper_value: stats?.avg + stats?.sd,
},
throughput: {
value: stats?.iters,
lower_value: stats?.iters - throughputSd,
upper_value: stats?.iters + throughputSd,
},
},
}
})
.reduce((obj, item) => Object.assign(obj, item), {})
}

export const checkBenchmarkArgs = (fn, opts = {}) => {
if (![Function, AsyncFunction].includes(fn.constructor))
throw new TypeError(`expected function, got ${fn.constructor.name}`)
Expand Down Expand Up @@ -203,18 +188,6 @@ export const overrideBenchmarkOptions = (benchmark, opts) => {
benchmark.warmup = opts.warmup ?? benchmark.warmup
}

export const isObject = value => {
return Object.prototype.toString.call(value).slice(8, -1) === 'Object'
}

export const checkDividend = n => {
if (n == null) throw new TypeError(`Invalid dividend: ${n}`)
if ('number' !== typeof n)
throw new TypeError(`expected number, got ${n.constructor.name}`)
if (n === 0 || Number.isNaN(n)) throw new RangeError(`Invalid dividend: ${n}`)
return n
}

/**
* Measure a function runtime.
*
Expand Down Expand Up @@ -297,68 +270,10 @@ export async function measure(fn, opts = {}) {
? await benchmark(fn, opts.before, opts.after, opts.now)
: benchmark(fn, opts.before, opts.after, opts.now)

return buildStats(samples)
}

const variance = (samples, avg = average(samples)) => {
return (
samples.reduce((a, b) => a + (b - avg) ** 2, 0) /
checkDividend(samples.length - 1) // Bessel's correction
)
}

const quantileSorted = (samples, q) => {
if (!Array.isArray(samples)) {
throw new TypeError(`expected array, got ${samples.constructor.name}`)
}
if (samples.length === 0) {
throw new Error('expected non-empty array, got empty array')
}
if (q < 0 || q > 1) {
throw new Error('q must be between 0 and 1')
}
if (q === 0) {
return samples[0]
}
if (q === 1) {
return samples[samples.length - 1]
}
const base = (samples.length - 1) * q
const baseIndex = Math.floor(base)
if (samples[baseIndex + 1] != null) {
return (
samples[baseIndex] +
(base - baseIndex) * (samples[baseIndex + 1] - samples[baseIndex])
)
}
return samples[baseIndex]
}

const medianSorted = samples => quantileSorted(samples, 0.5)

const average = samples => {
if (!Array.isArray(samples)) {
throw new TypeError(`expected array, got ${samples.constructor.name}`)
}
if (samples.length === 0) {
throw new Error('expected non-empty array, got empty array')
}

return samples.reduce((a, b) => a + b, 0) / samples.length
return buildMeasurementStats(samples)
}

const absoluteDeviation = (samples, aggFn) => {
const value = aggFn(samples)
const absoluteDeviations = []

for (const sample of samples) {
absoluteDeviations.push(Math.abs(sample - value))
}

return aggFn(absoluteDeviations)
}

const buildStats = samples => {
const buildMeasurementStats = samples => {
if (!Array.isArray(samples))
throw new TypeError(`expected array, got ${samples.constructor.name}`)
if (samples.length === 0)
Expand Down Expand Up @@ -394,13 +309,3 @@ const buildStats = samples => {
ss: samples.length >= minimumSamples,
}
}

// https://en.wikipedia.org/wiki/Propagation_of_uncertainty#Example_formulae
export const ratioStandardDeviation = (avgA, sdA, avgB, sdB) => {
return (
(avgA / checkDividend(avgB)) *
Math.sqrt(
(sdA / checkDividend(avgA)) ** 2 + (sdB / checkDividend(avgB)) ** 2
)
)
}
24 changes: 24 additions & 0 deletions src/reporter/json/bmf.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ratioStandardDeviation } from '../../stats-utils.js'

export const bmf = report => {
return report.benchmarks
.filter(benchmark => benchmark.error == null)
.map(({ name, stats }) => {
const throughputSd = ratioStandardDeviation(1e9, 0, stats?.avg, stats?.sd)
return {
[name]: {
latency: {
value: stats?.avg,
lower_value: stats?.avg - stats?.sd,
upper_value: stats?.avg + stats?.sd,
},
throughput: {
value: stats?.iters,
lower_value: stats?.iters - throughputSd,
upper_value: stats?.iters + throughputSd,
},
},
}
})
.reduce((obj, item) => Object.assign(obj, item), {})
}
1 change: 1 addition & 0 deletions src/reporter/json/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { bmf } from './bmf.js'
File renamed without changes.
File renamed without changes.
15 changes: 15 additions & 0 deletions src/reporter/terminal/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export {
bold,
dim,
white,
} from './clr.js'
export {
benchmarkError,
benchmarkReport,
br,
header,
size,
summary,
units,
warning,
} from './table.js'
Loading

0 comments on commit 25ac409

Please sign in to comment.