Skip to content
Open
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
2 changes: 1 addition & 1 deletion packages/ui/client/components/views/ViewModuleGraph.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const breakdownIconClass = computed(() => {
}
return textClass
})
const breakdownShow = ref(config.value?.experimental?.printImportBreakdown ?? breakdownIconClass.value === 'text-red')
const breakdownShow = ref(config.value?.experimental?.printImportBreakdown?.enabled ?? breakdownIconClass.value === 'text-red')

onMounted(() => {
filteredGraph.value = filterGraphByLevels(graph.value, null, 2)
Expand Down
16 changes: 16 additions & 0 deletions packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,22 @@ export const cliOptionsConfig: VitestCLIOptions = {
openTelemetry: null,
printImportBreakdown: {
description: 'Print import breakdown after the summary. If the reporter doesn\'t support summary, this will have no effect. Note that UI\'s "Module Graph" tab always has an import breakdown.',
argument: '',
transform(value) {
if (typeof value === 'boolean') {
return { enabled: value }
}
return value
},
subcommands: {
enabled: {
description: 'Enable import breakdown display (default: false).',
},
limit: {
description: 'Number of imports to show in CLI output (default: 10). Set to 0 to disable import duration collection entirely.',
argument: '<number>',
},
},
},
},
},
Expand Down
5 changes: 4 additions & 1 deletion packages/vitest/src/node/config/resolveConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,7 @@ export function resolveConfig(
resolved.testTimeout ??= resolved.browser.enabled ? 15_000 : 5_000
resolved.hookTimeout ??= resolved.browser.enabled ? 30_000 : 10_000

resolved.experimental ??= {}
resolved.experimental ??= {} as any
if (resolved.experimental.openTelemetry?.sdkPath) {
const sdkPath = resolve(
resolved.root,
Expand All @@ -823,6 +823,9 @@ export function resolveConfig(
resolved.experimental.fsModuleCachePath,
)
}
resolved.experimental.printImportBreakdown ??= {} as any
resolved.experimental.printImportBreakdown.enabled ??= false
resolved.experimental.printImportBreakdown.limit ??= 10
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be 0 by default then

I think we can just say "enable this option to see import breakdown" in the current popup

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that works too. It looks like I was just over-complicating things.


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

if (this.ctx.config.experimental.printImportBreakdown) {
if (this.ctx.config.experimental.printImportBreakdown.enabled) {
this.printImportsBreakdown()
}

Expand Down Expand Up @@ -649,14 +649,15 @@ export abstract class BaseReporter implements Reporter {

const sortedImports = allImports.sort((a, b) => b.totalTime - a.totalTime)
const maxTotalTime = sortedImports[0].totalTime
const topImports = sortedImports.slice(0, 10)
const limit = this.ctx.config.experimental.printImportBreakdown.limit
const topImports = sortedImports.slice(0, limit)

const totalSelfTime = allImports.reduce((sum, imp) => sum + imp.selfTime, 0)
const totalTotalTime = allImports.reduce((sum, imp) => sum + imp.totalTime, 0)
const slowestImport = sortedImports[0]

this.log()
this.log(c.bold('Import Duration Breakdown') + c.dim(' (ordered by Total Time) (Top 10)'))
this.log(c.bold('Import Duration Breakdown') + c.dim(` (ordered by Total Time) (Top ${limit})`))

// 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
Expand Down
23 changes: 21 additions & 2 deletions packages/vitest/src/node/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -850,11 +850,23 @@ export interface InlineConfig {
browserSdkPath?: string
}
/**
* Show imports (top 10) that take a long time.
* Show imports that take a long time.
*
* Enabling this will also show a breakdown by default in UI, but you can always press a button to toggle it.
*/
printImportBreakdown?: boolean
printImportBreakdown?: {
/**
* Enable import breakdown display in CLI output.
* @default false
*/
enabled?: boolean
/**
* Number of imports to show in CLI output.
* Set to 0 to disable import duration collection entirely.
* @default 10
*/
limit?: number
}
}
}

Expand Down Expand Up @@ -1093,6 +1105,13 @@ export interface ResolvedConfig

vmMemoryLimit?: UserConfig['vmMemoryLimit']
dumpDir?: string

experimental: Omit<Required<UserConfig>['experimental'], 'printImportBreakdown'> & {
printImportBreakdown: {
enabled: boolean
limit: number
}
}
}

type NonProjectOptions
Expand Down
5 changes: 4 additions & 1 deletion packages/vitest/src/runtime/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ export interface SerializedConfig {
serializedDefines: string
experimental: {
fsModuleCache: boolean
printImportBreakdown: boolean | undefined
printImportBreakdown: {
enabled: boolean
limit: number
}
openTelemetry: {
enabled: boolean
sdkPath?: string
Expand Down
21 changes: 18 additions & 3 deletions packages/vitest/src/runtime/runners/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,25 @@ export class VitestTestRunner implements VitestRunner {
}

getImportDurations(): Record<string, ImportDuration> {
const importDurations: Record<string, ImportDuration> = {}
const entries = this.workerState.moduleExecutionInfo?.entries() || []
const { limit } = this.config.experimental.printImportBreakdown
// limit = 0 means opt-out from collection entirely
if (limit === 0) {
return {}
}

const entries = [...(this.workerState.moduleExecutionInfo?.entries() || [])]

for (const [filepath, { duration, selfTime, external, importer }] of entries) {
// Trim to top N by duration to reduce IPC payload
// Keep enough entries for UI "show more" and aggregation
const retention = Math.max(50, limit * 5)
Copy link
Member

@sheremet-va sheremet-va Jan 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this makes sense, to be honest. If there is a limit, then there is a limit. Maybe remove "show more" in UI


// Sort by duration descending and keep top entries
const sortedEntries = entries
.sort(([, a], [, b]) => b.duration - a.duration)
.slice(0, retention)

const importDurations: Record<string, ImportDuration> = {}
for (const [filepath, { duration, selfTime, external, importer }] of sortedEntries) {
importDurations[normalize(filepath)] = {
selfTime,
totalTime: duration,
Expand Down
33 changes: 33 additions & 0 deletions test/reporters/tests/import-durations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,37 @@ describe('import durations', () => {
expect(file.importDurations?.[throwsFile]?.totalTime).toBeGreaterThanOrEqual(24)
expect(file.importDurations?.[throwsFile]?.selfTime).toBeGreaterThanOrEqual(24)
})

it('should print import breakdown when enabled', async () => {
const { stdout } = await runVitest({
root,
include: ['**/import-durations.test.ts'],
experimental: {
printImportBreakdown: {
enabled: true,
limit: 5,
},
},
})

expect(stdout).toContain('Import Duration Breakdown')
expect(stdout).toContain('(ordered by Total Time)')
expect(stdout).toContain('Total imports:')
expect(stdout).toContain('(Top 5)')
})

it('should not collect importDurations when limit is 0', async () => {
const { ctx } = await runVitest({
root,
include: ['**/import-durations.test.ts'],
experimental: {
printImportBreakdown: {
limit: 0,
},
},
})

const file = ctx!.state.getFiles()[0]
expect(file.importDurations).toEqual({})
})
})
Loading