From 39162c64e0aa111fb9c2972f36070c68f6dd1945 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 7 Jan 2026 15:50:03 +0900 Subject: [PATCH 1/2] feat: add `printImportBreakdown.limit` and truncate importDurations collection with reasonable size --- .../components/views/ViewModuleGraph.vue | 2 +- packages/vitest/src/node/cli/cli-config.ts | 16 +++++++++++++ .../vitest/src/node/config/resolveConfig.ts | 5 +++- packages/vitest/src/node/reporters/base.ts | 7 +++--- packages/vitest/src/node/types/config.ts | 23 +++++++++++++++++-- packages/vitest/src/runtime/config.ts | 5 +++- packages/vitest/src/runtime/runners/test.ts | 21 ++++++++++++++--- 7 files changed, 68 insertions(+), 11 deletions(-) diff --git a/packages/ui/client/components/views/ViewModuleGraph.vue b/packages/ui/client/components/views/ViewModuleGraph.vue index 2a9d3e1a21fc..879b8d696770 100644 --- a/packages/ui/client/components/views/ViewModuleGraph.vue +++ b/packages/ui/client/components/views/ViewModuleGraph.vue @@ -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) diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index a3e078e5b387..a329c6dea476 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -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: '', + }, + }, }, }, }, diff --git a/packages/vitest/src/node/config/resolveConfig.ts b/packages/vitest/src/node/config/resolveConfig.ts index 45e99c651dcb..449d8c77419e 100644 --- a/packages/vitest/src/node/config/resolveConfig.ts +++ b/packages/vitest/src/node/config/resolveConfig.ts @@ -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, @@ -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 return resolved } diff --git a/packages/vitest/src/node/reporters/base.ts b/packages/vitest/src/node/reporters/base.ts index e1b2beb94775..9c0384460ab9 100644 --- a/packages/vitest/src/node/reporters/base.ts +++ b/packages/vitest/src/node/reporters/base.ts @@ -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() } @@ -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 diff --git a/packages/vitest/src/node/types/config.ts b/packages/vitest/src/node/types/config.ts index fcd1f881083e..3b454e5cf65b 100644 --- a/packages/vitest/src/node/types/config.ts +++ b/packages/vitest/src/node/types/config.ts @@ -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 + } } } @@ -1093,6 +1105,13 @@ export interface ResolvedConfig vmMemoryLimit?: UserConfig['vmMemoryLimit'] dumpDir?: string + + experimental: Omit['experimental'], 'printImportBreakdown'> & { + printImportBreakdown: { + enabled: boolean + limit: number + } + } } type NonProjectOptions diff --git a/packages/vitest/src/runtime/config.ts b/packages/vitest/src/runtime/config.ts index 1e7cf9561eb9..3f954283ab20 100644 --- a/packages/vitest/src/runtime/config.ts +++ b/packages/vitest/src/runtime/config.ts @@ -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 diff --git a/packages/vitest/src/runtime/runners/test.ts b/packages/vitest/src/runtime/runners/test.ts index 477173388d03..2dff8c9b823c 100644 --- a/packages/vitest/src/runtime/runners/test.ts +++ b/packages/vitest/src/runtime/runners/test.ts @@ -227,10 +227,25 @@ export class VitestTestRunner implements VitestRunner { } getImportDurations(): Record { - const importDurations: Record = {} - 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) + + // Sort by duration descending and keep top entries + const sortedEntries = entries + .sort(([, a], [, b]) => b.duration - a.duration) + .slice(0, retention) + + const importDurations: Record = {} + for (const [filepath, { duration, selfTime, external, importer }] of sortedEntries) { importDurations[normalize(filepath)] = { selfTime, totalTime: duration, From 4a4eac13ced942eb9e25ac08ce88c98b52ac3354 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Wed, 7 Jan 2026 16:12:18 +0900 Subject: [PATCH 2/2] test: e2e --- test/reporters/tests/import-durations.test.ts | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/reporters/tests/import-durations.test.ts b/test/reporters/tests/import-durations.test.ts index 39306adb2879..a92e4afb3664 100644 --- a/test/reporters/tests/import-durations.test.ts +++ b/test/reporters/tests/import-durations.test.ts @@ -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({}) + }) })