Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 53 additions & 7 deletions docs/config/experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,17 +187,35 @@ Please leave feedback regarding this feature in a [GitHub Discussion](https://gi
```ts
interface ImportDurationsOptions {
/**
* Print import breakdown to CLI terminal after tests finish.
* When to print import breakdown to CLI terminal.
* - false: Never print (default)
* - true: Always print
* - 'on-warn': Print only when any import exceeds warn threshold
*/
print?: boolean
print?: boolean | 'on-warn'
/**
* Fail the test run if any import exceeds the danger threshold.
* When enabled and threshold exceeded, breakdown is always printed.
* @default false
*/
failOnDanger?: boolean
/**
* Maximum number of imports to collect and display.
*/
limit?: number
/**
* Duration thresholds in milliseconds for coloring and warnings.
*/
thresholds?: {
/** Threshold for yellow/warning color. @default 100 */
warn?: number
/** Threshold for red/danger color and failOnDanger. @default 500 */
danger?: number
}
}
```

- **Default:** `{ print: false, limit: 0 }` (`limit` is 10 if `print` or UI is enabled)
- **Default:** `{ print: false, failOnDanger: false, limit: 0, thresholds: { warn: 100, danger: 500 } }` (`limit` is 10 if `print` or UI is enabled)

Configure import duration collection and display.

Expand All @@ -206,26 +224,54 @@ The `print` option controls CLI terminal output. The `limit` option controls how
- Self: the time it took to import the module, excluding static imports;
- Total: the time it took to import the module, including static imports. Note that this does not include `transform` time of the current module.

<img alt="An example of import breakdown in the terminal" src="/reporter-import-breakdown.png" />
<img alt="An example of import breakdown in the terminal" src="/reporter-import-breakdown.png" img-dark />
<img alt="An example of import breakdown in the terminal" src="/reporter-import-breakdown-light.png" img-light />

Note that if the file path is too long, Vitest will truncate it at the start until it fits 45 character limit.

### experimental.importDurations.print {#experimental-importdurationsprint}

- **Type:** `boolean | 'on-warn'`
- **Default:** `false`

Controls when to print import breakdown to CLI terminal after tests finish. This only works with [`default`](/guide/reporters#default), [`verbose`](/guide/reporters#verbose), or [`tree`](/guide/reporters#tree) reporters.

- `false`: Never print breakdown
- `true`: Always print breakdown
- `'on-warn'`: Print only when any import exceeds the `thresholds.warn` value

### experimental.importDurations.failOnDanger {#experimental-importdurationsfailondanger}

- **Type:** `boolean`
- **Default:** `false`

Print import breakdown to CLI terminal after tests finish. This only works with [`default`](/guide/reporters#default), [`verbose`](/guide/reporters#verbose), or [`tree`](/guide/reporters#tree) reporters.
Fail the test run if any import exceeds the `thresholds.danger` value. When enabled and the threshold is exceeded, the breakdown is always printed regardless of the `print` setting.

This is useful for enforcing import performance budgets in CI:

```bash
vitest --experimental.importDurations.failOnDanger
```

### experimental.importDurations.limit {#experimental-importdurationslimit}

- **Type:** `number`
- **Default:** `0` (or `10` if `print` or UI is enabled)
- **Default:** `0` (or `10` if `print`, `failOnDanger`, or UI is enabled)

Maximum number of imports to collect and display in CLI output, [Vitest UI](/guide/ui#import-breakdown), and third-party reporters.

### experimental.importDurations.thresholds {#experimental-importdurationsthresholds}

- **Type:** `{ warn?: number; danger?: number }`
- **Default:** `{ warn: 100, danger: 500 }`

Duration thresholds in milliseconds for coloring and warnings:

- `warn`: Threshold for yellow/warning color (default: 100ms)
- `danger`: Threshold for red/danger color and `failOnDanger` (default: 500ms)

::: info
[Vitest UI](/guide/ui#import-breakdown) shows a breakdown of imports automatically if at least one file took longer than 500 milliseconds to load. You can manually set this option to `false` to disable this.
[Vitest UI](/guide/ui#import-breakdown) shows a breakdown of imports automatically if at least one file took longer than the `danger` threshold to load.
:::

## experimental.viteModuleRunner <Version type="experimental">4.1.0</Version> {#experimental-vitemodulerunner}
Expand Down
25 changes: 23 additions & 2 deletions docs/guide/cli-generated.md
Original file line number Diff line number Diff line change
Expand Up @@ -840,10 +840,10 @@ Enable caching of modules on the file system between reruns.

### experimental.importDurations.print

- **CLI:** `--experimental.importDurations.print`
- **CLI:** `--experimental.importDurations.print <boolean|on-warn>`
- **Config:** [experimental.importDurations.print](/config/experimental#experimental-importdurations-print)

Print import breakdown to CLI terminal after tests finish (default: false).
When to print import breakdown to CLI terminal. Use `true` to always print, `false` to never print, or `on-warn` to print only when imports exceed the warn threshold (default: false).

### experimental.importDurations.limit

Expand All @@ -852,6 +852,27 @@ Print import breakdown to CLI terminal after tests finish (default: false).

Maximum number of imports to collect and display (default: 0, or 10 if print or UI is enabled).

### experimental.importDurations.failOnDanger

- **CLI:** `--experimental.importDurations.failOnDanger`
- **Config:** [experimental.importDurations.failOnDanger](/config/experimental#experimental-importdurations-failondanger)

Fail the test run if any import exceeds the danger threshold (default: false).

### experimental.importDurations.thresholds.warn

- **CLI:** `--experimental.importDurations.thresholds.warn <number>`
- **Config:** [experimental.importDurations.thresholds.warn](/config/experimental#experimental-importdurations-thresholds-warn)

Warning threshold - imports exceeding this are shown in yellow/orange (default: 100).

### experimental.importDurations.thresholds.danger

- **CLI:** `--experimental.importDurations.thresholds.danger <number>`
- **Config:** [experimental.importDurations.thresholds.danger](/config/experimental#experimental-importdurations-thresholds-danger)

Danger threshold - imports exceeding this are shown in red (default: 500).

### experimental.viteModuleRunner

- **CLI:** `--experimental.viteModuleRunner`
Expand Down
4 changes: 2 additions & 2 deletions docs/guide/ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ If the module was inlined, you will see three more windows:

All static imports in the "Source" window show a total time it took to evaluate them by the current module. If the import was already evaluated in the module graph, it will show `0ms` because it is cached by that point.

If the module took longer than 500 milliseconds to load, the time will be displayed in red. If the module took longer than 100 milliseconds, the time will be displayed in orange.
If the module took longer than the [`danger` threshold](/config/experimental#experimental-importdurations-thresholds) (default: 500ms) to load, the time will be displayed in red. If the module took longer than the [`warn` threshold](/config/experimental#experimental-importdurations-thresholds) (default: 100ms), the time will be displayed in orange.

You can click on an import source to jump into that module and traverse the graph further (note `./support/assertions/index.ts` below).

Expand Down Expand Up @@ -142,6 +142,6 @@ You can click on the module to see the Module Info. If the module is external, i

The breakdown shows a list of modules with self time, total time, and a percentage relative to the time it took to load the whole test file.

The "Show Import Breakdown" icon will have a red color if there is at least one file that took longer than 500 milliseconds to load, and it will be orange if there is at least one file that took longer than 100 milliseconds.
The "Show Import Breakdown" icon will have a red color if there is at least one file that took longer than the [`danger` threshold](/config/experimental#experimental-importdurations-thresholds) (default: 500ms) to load, and it will be orange if there is at least one file that took longer than the [`warn` threshold](/config/experimental#experimental-importdurations-thresholds) (default: 100ms).

You can use [`experimental.importDurations.limit`](/config/experimental#experimental-importdurationslimit) to control the number of imports displayed.
Binary file added docs/public/reporter-import-breakdown-light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/public/reporter-import-breakdown.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 8 additions & 4 deletions packages/ui/client/components/views/ViewModuleGraph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
PositionInitializers,
} from 'd3-graph-controller'
import { computed, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue'
import { isReport } from '~/composables/client'
import { config, isReport } from '~/composables/client'
import { currentModule } from '~/composables/navigation'
import IconButton from '../IconButton.vue'
import Modal from '../Modal.vue'
Expand All @@ -43,14 +43,18 @@ const focusedNode = ref<string | null>(null)
const filteredGraph = shallowRef<ModuleGraph>(graph.value)
const breakdownIconClass = computed(() => {
let textClass = ''
const importDurations = currentModule.value?.importDurations || {}
const importDurations = currentModule.value?.importDurations
if (!importDurations) {
return textClass
}
const thresholds = config.value.experimental.importDurations.thresholds
for (const moduleId in importDurations) {
const { totalTime } = importDurations[moduleId]
if (totalTime >= 500) {
if (totalTime >= thresholds.danger) {
textClass = 'text-red'
break
}
else if (totalTime >= 100) {
else if (totalTime >= thresholds.warn) {
textClass = 'text-orange'
}
}
Expand Down
26 changes: 25 additions & 1 deletion packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -821,12 +821,36 @@ export const cliOptionsConfig: VitestCLIOptions = {
},
subcommands: {
print: {
description: 'Print import breakdown to CLI terminal after tests finish (default: false).',
description: 'When to print import breakdown to CLI terminal. Use `true` to always print, `false` to never print, or `on-warn` to print only when imports exceed the warn threshold (default: false).',
argument: '<boolean|on-warn>',
transform(value) {
if (value === 'on-warn') {
return 'on-warn'
}
return value
},
},
limit: {
description: 'Maximum number of imports to collect and display (default: 0, or 10 if print or UI is enabled).',
argument: '<number>',
},
failOnDanger: {
description: 'Fail the test run if any import exceeds the danger threshold (default: false).',
},
thresholds: {
description: 'Duration thresholds in milliseconds for coloring and warnings.',
argument: '',
subcommands: {
warn: {
description: 'Warning threshold - imports exceeding this are shown in yellow/orange (default: 100).',
argument: '<number>',
},
danger: {
description: 'Danger threshold - imports exceeding this are shown in red (default: 500).',
argument: '<number>',
},
},
},
},
},
viteModuleRunner: {
Expand Down
9 changes: 8 additions & 1 deletion packages/vitest/src/node/config/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,10 +869,17 @@ export function resolveConfig(
}
resolved.experimental.importDurations ??= {} as any
resolved.experimental.importDurations.print ??= false
resolved.experimental.importDurations.failOnDanger ??= false
if (resolved.experimental.importDurations.limit == null) {
const shouldCollect = resolved.experimental.importDurations.print || resolved.ui
const shouldCollect
= resolved.experimental.importDurations.print
|| resolved.experimental.importDurations.failOnDanger
|| resolved.ui
resolved.experimental.importDurations.limit = shouldCollect ? 10 : 0
}
resolved.experimental.importDurations.thresholds ??= {} as any
resolved.experimental.importDurations.thresholds.warn ??= 100
resolved.experimental.importDurations.thresholds.danger ??= 500

return resolved
}
Expand Down
65 changes: 47 additions & 18 deletions packages/vitest/src/node/reporters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,14 +607,17 @@ export abstract class BaseReporter implements Reporter {
}
}

if (this.ctx.config.experimental.importDurations.print) {
this.printImportsBreakdown()
}
this.reportImportDurations()

this.log()
}

private printImportsBreakdown() {
private reportImportDurations() {
const { print, failOnDanger, thresholds } = this.ctx.config.experimental.importDurations
if (!print && !failOnDanger) {
return
};

const testModules = this.ctx.state.getTestModules()

interface ImportEntry {
Expand Down Expand Up @@ -647,6 +650,18 @@ export abstract class BaseReporter implements Reporter {
return
}

const dangerImports = allImports.filter(imp => imp.totalTime >= thresholds.danger)
const warnImports = allImports.filter(imp => imp.totalTime >= thresholds.warn)
const hasDangerImports = dangerImports.length > 0
const hasWarnImports = warnImports.length > 0

// Determine if we should print
const shouldFail = failOnDanger && hasDangerImports
const shouldPrint = (print === true) || (print === 'on-warn' && hasWarnImports) || shouldFail
if (!shouldPrint) {
return
}

const sortedImports = allImports.sort((a, b) => b.totalTime - a.totalTime)
const maxTotalTime = sortedImports[0].totalTime
const limit = this.ctx.config.experimental.importDurations.limit
Expand All @@ -657,21 +672,25 @@ export abstract class BaseReporter implements Reporter {
const slowestImport = sortedImports[0]

this.log()
this.log(c.bold('Import Duration Breakdown') + c.dim(` (ordered by Total Time) (Top ${limit})`))
this.log(c.bold('Import Duration Breakdown') + c.dim(` (Top ${limit})`))
this.log()
this.log(c.dim(`${'Module'.padEnd(50)} ${'Self'.padStart(6)} ${'Total'.padStart(6)}`))

// if there are multiple files, it's highly possible that some of them will import the same large file
// we group them to show the distinction between those files more easily
// Import Duration Breakdown (ordered by Total Time) (Top 10)
// .../fields/FieldFile/__tests__/FieldFile.spec.ts self: 7ms total: 1.01s ████████████████████
// ↳ tests/support/components/index.ts self: 0ms total: 861ms █████████████████░░░
// ↳ tests/support/components/renderComponent.ts self: 59ms total: 861ms █████████████████░░░
// ...s__/apps/desktop/form-updater.desktop.spec.ts self: 8ms total: 991ms ████████████████████
// ...sts__/apps/mobile/form-updater.mobile.spec.ts self: 11ms total: 990ms ████████████████████
// shared/components/Form/__tests__/Form.spec.ts self: 5ms total: 988ms ████████████████████
// ↳ tests/support/components/index.ts self: 0ms total: 935ms ███████████████████░
// ↳ tests/support/components/renderComponent.ts self: 61ms total: 935ms ███████████████████░
// ...ditor/features/link/__test__/LinkForm.spec.ts self: 7ms total: 972ms ███████████████████░
// ↳ tests/support/components/renderComponent.ts self: 56ms total: 936ms ███████████████████░
// Import Duration Breakdown (Top 10)
//
// Module Self Total
// .../fields/FieldFile/__tests__/FieldFile.spec.ts 7ms 1.01s ████████████████████
// ↳ tests/support/components/index.ts 0ms 861ms █████████████████░░░
// ↳ tests/support/components/renderComponent.ts 59ms 861ms █████████████████░░░
// ...s__/apps/desktop/form-updater.desktop.spec.ts 8ms 991ms ████████████████████
// ...sts__/apps/mobile/form-updater.mobile.spec.ts 11ms 990ms ████████████████████
// shared/components/Form/__tests__/Form.spec.ts 5ms 988ms ████████████████████
// ↳ tests/support/components/index.ts 0ms 935ms ███████████████████░
// ↳ tests/support/components/renderComponent.ts 61ms 935ms ███████████████████░
// ...ditor/features/link/__test__/LinkForm.spec.ts 7ms 972ms ███████████████████░
// ↳ tests/support/components/renderComponent.ts 56ms 936ms ███████████████████░

const groupedImports = Object.entries(
groupBy(topImports, i => i.testModule.id),
Expand All @@ -688,7 +707,7 @@ export abstract class BaseReporter implements Reporter {
const pathDisplay = this.ellipsisPath(imp.importedModuleId, imp.external, groupedImports.length > 1 && index > 0)

this.log(
`${pathDisplay} ${c.dim('self:')} ${this.importDurationTime(imp.selfTime)} ${c.dim('total:')} ${this.importDurationTime(imp.totalTime)} ${bar}`,
`${pathDisplay} ${this.importDurationTime(imp.selfTime)} ${this.importDurationTime(imp.totalTime)} ${bar}`,
)
})
}
Expand All @@ -697,10 +716,20 @@ export abstract class BaseReporter implements Reporter {
this.log(c.dim('Total imports: ') + allImports.length)
this.log(c.dim('Slowest import (total-time): ') + formatTime(slowestImport.totalTime))
this.log(c.dim('Total import time (self/total): ') + formatTime(totalSelfTime) + c.dim(' / ') + formatTime(totalTotalTime))

// Fail if danger threshold exceeded
if (shouldFail) {
this.log()
this.ctx.logger.error(
`ERROR: ${dangerImports.length} import(s) exceeded the danger threshold of ${thresholds.danger}ms`,
)
process.exitCode = 1
}
}

private importDurationTime(duration: number) {
const color = duration >= 500 ? c.red : duration >= 100 ? c.yellow : (c: string) => c
const { thresholds } = this.ctx.config.experimental.importDurations
const color = duration >= thresholds.danger ? c.red : duration >= thresholds.warn ? c.yellow : (c: string) => c
return color(formatTime(duration).padStart(6))
}

Expand Down
Loading
Loading