From 5b1ff43e661a7792233a035ca657dfc6347149af Mon Sep 17 00:00:00 2001 From: yoho Date: Mon, 7 Nov 2022 20:23:13 +0800 Subject: [PATCH] feat: benchmark table report (#2179) fix https://github.com/vitest-dev/vitest/issues/2005 --- .../src/node/reporters/benchmark/index.ts | 5 +- .../node/reporters/benchmark/table/index.ts | 60 ++++++ .../reporters/benchmark/table/tableRender.ts | 200 ++++++++++++++++++ test/benchmark/package.json | 3 +- test/benchmark/test/only.bench.ts | 8 + test/benchmark/vitest.config.ts | 2 +- 6 files changed, 275 insertions(+), 3 deletions(-) create mode 100644 packages/vitest/src/node/reporters/benchmark/table/index.ts create mode 100644 packages/vitest/src/node/reporters/benchmark/table/tableRender.ts diff --git a/packages/vitest/src/node/reporters/benchmark/index.ts b/packages/vitest/src/node/reporters/benchmark/index.ts index 95a76d5ad001..00aa184557b4 100644 --- a/packages/vitest/src/node/reporters/benchmark/index.ts +++ b/packages/vitest/src/node/reporters/benchmark/index.ts @@ -1,7 +1,10 @@ import { VerboseReporter } from '../verbose' import { JsonReporter } from './json' +import { TableReporter } from './table' + export const BenchmarkReportsMap = { - default: VerboseReporter, + default: TableReporter, + verbose: VerboseReporter, json: JsonReporter, } export type BenchmarkBuiltinReporters = keyof typeof BenchmarkReportsMap diff --git a/packages/vitest/src/node/reporters/benchmark/table/index.ts b/packages/vitest/src/node/reporters/benchmark/table/index.ts new file mode 100644 index 000000000000..86d363130054 --- /dev/null +++ b/packages/vitest/src/node/reporters/benchmark/table/index.ts @@ -0,0 +1,60 @@ +import c from 'picocolors' +import type { UserConsoleLog } from '../../../../types' +import { BaseReporter } from '../../base' +import type { ListRendererOptions } from '../../renderers/listRenderer' +import { createTableRenderer } from './tableRender' + +export class TableReporter extends BaseReporter { + renderer?: ReturnType + rendererOptions: ListRendererOptions = {} as any + + async onTestRemoved(trigger?: string) { + await this.stopListRender() + this.ctx.logger.clearScreen(c.yellow('Test removed...') + (trigger ? c.dim(` [ ${this.relative(trigger)} ]\n`) : ''), true) + const files = this.ctx.state.getFiles(this.watchFilters) + createTableRenderer(files, this.rendererOptions).stop() + this.ctx.logger.log() + await super.reportSummary(files) + super.onWatcherStart() + } + + onCollected() { + if (this.isTTY) { + this.rendererOptions.logger = this.ctx.logger + this.rendererOptions.showHeap = this.ctx.config.logHeapUsage + const files = this.ctx.state.getFiles(this.watchFilters) + if (!this.renderer) + this.renderer = createTableRenderer(files, this.rendererOptions).start() + else + this.renderer.update(files) + } + } + + async onFinished(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) { + await this.stopListRender() + this.ctx.logger.log() + await super.onFinished(files, errors) + } + + async onWatcherStart() { + await this.stopListRender() + await super.onWatcherStart() + } + + async stopListRender() { + await this.renderer?.stop() + this.renderer = undefined + } + + async onWatcherRerun(files: string[], trigger?: string) { + await this.stopListRender() + await super.onWatcherRerun(files, trigger) + } + + onUserConsoleLog(log: UserConsoleLog) { + if (!this.shouldLog(log)) + return + this.renderer?.clear() + super.onUserConsoleLog(log) + } +} diff --git a/packages/vitest/src/node/reporters/benchmark/table/tableRender.ts b/packages/vitest/src/node/reporters/benchmark/table/tableRender.ts new file mode 100644 index 000000000000..aaed86f520c2 --- /dev/null +++ b/packages/vitest/src/node/reporters/benchmark/table/tableRender.ts @@ -0,0 +1,200 @@ +import c from 'picocolors' +import cliTruncate from 'cli-truncate' +import stripAnsi from 'strip-ansi' +import type { Benchmark, BenchmarkResult, Task } from '../../../../types' +import { clearInterval, getTests, notNullish, setInterval } from '../../../../utils' +import { F_RIGHT } from '../../../../utils/figures' +import type { Logger } from '../../../logger' +import { getCols, getStateSymbol } from '../../renderers/utils' + +export interface ListRendererOptions { + renderSucceed?: boolean + logger: Logger + showHeap: boolean +} + +const DURATION_LONG = 300 + +const outputMap = new WeakMap() + +function formatFilepath(path: string) { + const lastSlash = Math.max(path.lastIndexOf('/') + 1, 0) + const basename = path.slice(lastSlash) + let firstDot = basename.indexOf('.') + if (firstDot < 0) + firstDot = basename.length + firstDot += lastSlash + + return c.dim(path.slice(0, lastSlash)) + path.slice(lastSlash, firstDot) + c.dim(path.slice(firstDot)) +} + +function formatNumber(number: number) { + const res = String(number.toFixed(number < 100 ? 4 : 2)).split('.') + return res[0].replace(/(?=(?:\d{3})+$)(?!\b)/g, ',') + + (res[1] ? `.${res[1]}` : '') +} + +const tableHead = ['name', 'hz', 'min', 'max', 'mean', 'p75', 'p99', 'p995', 'p999', 'rme', 'samples'] + +function renderTableHead(tasks: Task[]) { + const benchs = tasks + .map(i => i.type === 'benchmark' ? i.result?.benchmark : undefined) + .filter(notNullish) + const allItems = benchs.map(renderBenchmarkItems).concat([tableHead]) + return `${' '.repeat(3)}${tableHead.map((i, idx) => { + const width = Math.max(...allItems.map(i => i[idx].length)) + return idx + ? i.padStart(width, ' ') + : i.padEnd(width, ' ') // name + }).map(c.bold).join(' ')}` +} + +function renderBenchmarkItems(result: BenchmarkResult) { + return [ + result.name, + formatNumber(result.hz || 0), + formatNumber(result.min || 0), + formatNumber(result.max || 0), + formatNumber(result.mean || 0), + formatNumber(result.p75 || 0), + formatNumber(result.p99 || 0), + formatNumber(result.p995 || 0), + formatNumber(result.p999 || 0), + `±${(result.rme || 0).toFixed(2)}%`, + result.samples.length.toString(), + ] +} +function renderBenchmark(task: Benchmark, tasks: Task[]): string { + const result = task.result?.benchmark + if (!result) + return task.name + + const benchs = tasks + .map(i => i.type === 'benchmark' ? i.result?.benchmark : undefined) + .filter(notNullish) + const allItems = benchs.map(renderBenchmarkItems).concat([tableHead]) + const items = renderBenchmarkItems(result) + const padded = items.map((i, idx) => { + const width = Math.max(...allItems.map(i => i[idx].length)) + return idx + ? i.padStart(width, ' ') + : i.padEnd(width, ' ') // name + }) + + return [ + padded[0], // name + c.blue(padded[1]), // hz + c.cyan(padded[2]), // min + c.cyan(padded[3]), // max + c.cyan(padded[4]), // mean + c.cyan(padded[5]), // p75 + c.cyan(padded[6]), // p99 + c.cyan(padded[7]), // p995 + c.cyan(padded[8]), // p999 + c.dim(padded[9]), // rem + c.dim(padded[10]), // sample + result.rank === 1 + ? c.bold(c.green(' fastest')) + : result.rank === benchs.length && benchs.length > 2 + ? c.bold(c.gray(' slowest')) + : '', + ].join(' ') +} + +export function renderTree(tasks: Task[], options: ListRendererOptions, level = 0) { + let output: string[] = [] + + let idx = 0 + for (const task of tasks) { + const padding = ' '.repeat(level ? 1 : 0) + let prefix = '' + if (idx === 0 && task.type === 'benchmark') + prefix += `${renderTableHead(tasks)}\n${padding}` + + prefix += ` ${getStateSymbol(task)} ` + + let suffix = '' + if (task.type === 'suite') + suffix += c.dim(` (${getTests(task).length})`) + + if (task.mode === 'skip' || task.mode === 'todo') + suffix += ` ${c.dim(c.gray('[skipped]'))}` + + if (task.result?.duration != null) { + if (task.result.duration > DURATION_LONG) + suffix += c.yellow(` ${Math.round(task.result.duration)}${c.dim('ms')}`) + } + + if (options.showHeap && task.result?.heap != null) + suffix += c.magenta(` ${Math.floor(task.result.heap / 1024 / 1024)} MB heap used`) + + let name = task.name + if (level === 0) + name = formatFilepath(name) + + const body = task.type === 'benchmark' + ? renderBenchmark(task, tasks) + : name + + output.push(padding + prefix + body + suffix) + + if ((task.result?.state !== 'pass') && outputMap.get(task) != null) { + let data: string | undefined = outputMap.get(task) + if (typeof data === 'string') { + data = stripAnsi(data.trim().split('\n').filter(Boolean).pop()!) + if (data === '') + data = undefined + } + + if (data != null) { + const out = `${' '.repeat(level)}${F_RIGHT} ${data}` + output.push(` ${c.gray(cliTruncate(out, getCols(-3)))}`) + } + } + + if (task.type === 'suite' && task.tasks.length > 0) { + if (task.result?.state) + output = output.concat(renderTree(task.tasks, options, level + 1)) + } + idx++ + } + + return output.filter(Boolean).join('\n') +} + +export const createTableRenderer = (_tasks: Task[], options: ListRendererOptions) => { + let tasks = _tasks + let timer: any + + const log = options.logger.logUpdate + + function update() { + log(renderTree(tasks, options)) + } + + return { + start() { + if (timer) + return this + timer = setInterval(update, 200) + return this + }, + update(_tasks: Task[]) { + tasks = _tasks + update() + return this + }, + async stop() { + if (timer) { + clearInterval(timer) + timer = undefined + } + log.clear() + options.logger.log(renderTree(tasks, options)) + return this + }, + clear() { + log.clear() + }, + } +} diff --git a/test/benchmark/package.json b/test/benchmark/package.json index e513b3357980..16b0a4d04e74 100644 --- a/test/benchmark/package.json +++ b/test/benchmark/package.json @@ -3,7 +3,8 @@ "private": true, "scripts": { "test": "node test.mjs", - "bench": "vitest bench --reporter=json", + "bench:json": "vitest bench --reporter=json", + "bench": "vitest bench", "coverage": "vitest run --coverage" }, "devDependencies": { diff --git a/test/benchmark/test/only.bench.ts b/test/benchmark/test/only.bench.ts index bf012cace8b5..0ecd4faa688e 100644 --- a/test/benchmark/test/only.bench.ts +++ b/test/benchmark/test/only.bench.ts @@ -66,3 +66,11 @@ bench.only( }, { iterations: 1, time: 0 }, ) + +bench.only( + 'visited2', + () => { + assert.deepEqual(run, [true, true, true, true, true]) + }, + { iterations: 1, time: 0 }, +) diff --git a/test/benchmark/vitest.config.ts b/test/benchmark/vitest.config.ts index 8db62edd4234..163fd65bc16f 100644 --- a/test/benchmark/vitest.config.ts +++ b/test/benchmark/vitest.config.ts @@ -19,7 +19,7 @@ export default defineConfig({ onWatcherRerun: noop, onServerRestart: noop, onUserConsoleLog: noop, - }], + }, 'default'], }, }, })