From 22ca0b6bfe71308b0179874363221db690618f7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ari=20Perkki=C3=B6?= Date: Wed, 15 Mar 2023 21:37:32 +0200 Subject: [PATCH] perf(reporters): overall improvements (#3006) --- .../reporters/benchmark/table/tableRender.ts | 6 +- .../node/reporters/renderers/dotRenderer.ts | 74 ++++++++++++++----- .../node/reporters/renderers/listRenderer.ts | 49 ++++++++---- 3 files changed, 93 insertions(+), 36 deletions(-) diff --git a/packages/vitest/src/node/reporters/benchmark/table/tableRender.ts b/packages/vitest/src/node/reporters/benchmark/table/tableRender.ts index 4a2c825a9613..feeb65d18bdd 100644 --- a/packages/vitest/src/node/reporters/benchmark/table/tableRender.ts +++ b/packages/vitest/src/node/reporters/benchmark/table/tableRender.ts @@ -101,8 +101,8 @@ function renderBenchmark(task: Benchmark, tasks: Task[]): string { ].join(' ') } -export function renderTree(tasks: Task[], options: ListRendererOptions, level = 0) { - let output: string[] = [] +export function renderTree(tasks: Task[], options: ListRendererOptions, level = 0): string { + const output: string[] = [] let idx = 0 for (const task of tasks) { @@ -154,7 +154,7 @@ export function renderTree(tasks: Task[], options: ListRendererOptions, level = if (task.type === 'suite' && task.tasks.length > 0) { if (task.result?.state) - output = output.concat(renderTree(task.tasks, options, level + 1)) + output.push(renderTree(task.tasks, options, level + 1)) } idx++ } diff --git a/packages/vitest/src/node/reporters/renderers/dotRenderer.ts b/packages/vitest/src/node/reporters/renderers/dotRenderer.ts index fd0964ce2e1a..8ca3c888026b 100644 --- a/packages/vitest/src/node/reporters/renderers/dotRenderer.ts +++ b/packages/vitest/src/node/reporters/renderers/dotRenderer.ts @@ -7,25 +7,64 @@ export interface DotRendererOptions { logger: Logger } -const check = c.green('·') -const cross = c.red('x') -const pending = c.yellow('*') -const skip = c.dim(c.gray('-')) +interface Icon { char: string; color: (char: string) => string } -function render(tasks: Task[]) { +const check: Icon = { char: '·', color: c.green } +const cross: Icon = { char: 'x', color: c.red } +const pending: Icon = { char: '*', color: c.yellow } +const skip: Icon = { char: '-', color: (char: string) => c.dim(c.gray(char)) } + +function getIcon(task: Task) { + if (task.mode === 'skip' || task.mode === 'todo') + return skip + switch (task.result?.state) { + case 'pass': + return check + case 'fail': + return cross + default: + return pending + } +} + +function render(tasks: Task[]): string { const all = getTests(tasks) - return all.map((i) => { - if (i.mode === 'skip' || i.mode === 'todo') - return skip - switch (i.result?.state) { - case 'pass': - return check - case 'fail': - return cross - default: - return pending + const output: string[] = [] + + // The log-update uses various ANSI helper utilities, e.g. ansi-warp, ansi-slice, + // when printing. Passing it hundreds of single characters containing ANSI codes reduces + // performances. We can optimize it by reducing amount of ANSI codes, e.g. by coloring + // multiple tasks at once instead of each task separately. + let currentIcon = pending + let currentTasks = 0 + + const addOutput = () => output.push(currentIcon.color(currentIcon.char.repeat(currentTasks))) + + for (const task of all) { + const icon = getIcon(task) + const isLast = all.indexOf(task) === all.length - 1 + + if (icon === currentIcon) { + currentTasks++ + + if (isLast) + addOutput() + + continue } - }).join('') + + // Task mode/state has changed, add previous group to output + addOutput() + + // Start tracking new group + currentTasks = 1 + currentIcon = icon + + if (isLast) + addOutput() + } + + return output.join('') } export const createDotRenderer = (_tasks: Task[], options: DotRendererOptions) => { @@ -42,12 +81,11 @@ export const createDotRenderer = (_tasks: Task[], options: DotRendererOptions) = start() { if (timer) return this - timer = setInterval(update, 200) + timer = setInterval(update, 16) return this }, update(_tasks: Task[]) { tasks = _tasks - update() return this }, async stop() { diff --git a/packages/vitest/src/node/reporters/renderers/listRenderer.ts b/packages/vitest/src/node/reporters/renderers/listRenderer.ts index 5e9ca09c057f..231f98ac3350 100644 --- a/packages/vitest/src/node/reporters/renderers/listRenderer.ts +++ b/packages/vitest/src/node/reporters/renderers/listRenderer.ts @@ -35,7 +35,7 @@ function formatNumber(number: number) { + (res[1] ? `.${res[1]}` : '') } -function renderHookState(task: Task, hookName: keyof SuiteHooks, level = 0) { +function renderHookState(task: Task, hookName: keyof SuiteHooks, level = 0): string { const state = task.result?.hooks?.[hookName] if (state && state === 'run') return `${' '.repeat(level)} ${getHookStateSymbol(task, hookName)} ${c.dim(`[ ${hookName} ]`)}` @@ -86,10 +86,14 @@ function renderBenchmark(task: Benchmark, tasks: Task[]): string { ].join('') } -export function renderTree(tasks: Task[], options: ListRendererOptions, level = 0) { - let output: string[] = [] +export function renderTree(tasks: Task[], options: ListRendererOptions, level = 0, maxRows?: number): string { + const output: string[] = [] + let currentRowCount = 0 + + // Go through tasks in reverse order since maxRows is used to bail out early when limit is reached + for (const task of [...tasks].reverse()) { + const taskOutput = [] - for (const task of tasks) { let suffix = '' let prefix = ` ${getStateSymbol(task)} ` @@ -124,7 +128,7 @@ export function renderTree(tasks: Task[], options: ListRendererOptions, level = ? renderBenchmark(task as Benchmark, tasks) : name - output.push(padding + prefix + body + suffix) + taskOutput.push(padding + prefix + body + suffix) if ((task.result?.state !== 'pass') && outputMap.get(task) != null) { let data: string | undefined = outputMap.get(task) @@ -136,22 +140,29 @@ export function renderTree(tasks: Task[], options: ListRendererOptions, level = if (data != null) { const out = `${' '.repeat(level)}${F_RIGHT} ${data}` - output.push(` ${c.gray(cliTruncate(out, getCols(-3)))}`) + taskOutput.push(` ${c.gray(cliTruncate(out, getCols(-3)))}`) } } - output = output.concat(renderHookState(task, 'beforeAll', level + 1)) - output = output.concat(renderHookState(task, 'beforeEach', level + 1)) + taskOutput.push(renderHookState(task, 'beforeAll', level + 1)) + taskOutput.push(renderHookState(task, 'beforeEach', level + 1)) if (task.type === 'suite' && task.tasks.length > 0) { if ((task.result?.state === 'fail' || task.result?.state === 'run' || options.renderSucceed)) - output = output.concat(renderTree(task.tasks, options, level + 1)) + taskOutput.push(renderTree(task.tasks, options, level + 1, maxRows)) } - output = output.concat(renderHookState(task, 'afterAll', level + 1)) - output = output.concat(renderHookState(task, 'afterEach', level + 1)) + taskOutput.push(renderHookState(task, 'afterAll', level + 1)) + taskOutput.push(renderHookState(task, 'afterEach', level + 1)) + + const rows = taskOutput.filter(Boolean) + output.push(rows.join('\n')) + currentRowCount += rows.length + + if (maxRows && currentRowCount >= maxRows) + break } // TODO: moving windows - return output.filter(Boolean).join('\n') + return output.reverse().join('\n') } export const createListRenderer = (_tasks: Task[], options: ListRendererOptions) => { @@ -161,19 +172,25 @@ export const createListRenderer = (_tasks: Task[], options: ListRendererOptions) const log = options.logger.logUpdate function update() { - log(renderTree(tasks, options)) + log(renderTree( + tasks, + options, + 0, + // log-update already limits the amount of printed rows to fit the current terminal + // but we can optimize performance by doing it ourselves + process.stdout.rows, + )) } return { start() { if (timer) return this - timer = setInterval(update, 200) + timer = setInterval(update, 16) return this }, update(_tasks: Task[]) { tasks = _tasks - update() return this }, async stop() { @@ -182,6 +199,8 @@ export const createListRenderer = (_tasks: Task[], options: ListRendererOptions) timer = undefined } log.clear() + + // Note that at this point the renderTree should output all tasks options.logger.log(renderTree(tasks, options)) return this },