From 88a85a5d653902dae1e5c23a87dfae3313e09b53 Mon Sep 17 00:00:00 2001 From: Benjamin Newman Date: Thu, 12 Feb 2026 22:39:04 -0800 Subject: [PATCH 1/3] perf(ssr): skip circular import check for already-evaluated modules --- packages/vite/src/module-runner/runner.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/vite/src/module-runner/runner.ts b/packages/vite/src/module-runner/runner.ts index 8a65a378bc7840..64b97e2e3b63d5 100644 --- a/packages/vite/src/module-runner/runner.ts +++ b/packages/vite/src/module-runner/runner.ts @@ -182,7 +182,12 @@ export class ModuleRunner { if (importee) importers.add(importee) - // check circular dependency + // fast path: already evaluated modules can't deadlock + if (mod.evaluated && mod.promise) { + return this.processImport(await mod.promise, meta, metadata) + } + + // check circular dependency (only for modules still being evaluated) if ( callstack.includes(moduleId) || this.isCircularModule(mod) || @@ -207,7 +212,7 @@ export class ModuleRunner { } try { - // cached module + // cached module (in-progress, not yet evaluated) if (mod.promise) return this.processImport(await mod.promise, meta, metadata) From 8c0fd310bc81ae1aa91ae6824e0c4dfd16228a5d Mon Sep 17 00:00:00 2001 From: Benjamin Newman Date: Fri, 13 Feb 2026 05:57:54 -0800 Subject: [PATCH 2/3] docs: add isCircularImport benchmark script --- .../__tests__/bench-circular-import.ts | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 packages/vite/src/module-runner/__tests__/bench-circular-import.ts diff --git a/packages/vite/src/module-runner/__tests__/bench-circular-import.ts b/packages/vite/src/module-runner/__tests__/bench-circular-import.ts new file mode 100644 index 00000000000000..35ba5647c62653 --- /dev/null +++ b/packages/vite/src/module-runner/__tests__/bench-circular-import.ts @@ -0,0 +1,322 @@ +/** + * Benchmark for isCircularImport performance in Vite's ModuleRunner. + * + * Creates synthetic module graphs at various scales and measures the + * total time to evaluate all modules via ModuleRunner.import(). + * + * Usage: + * node --import tsx packages/vite/src/module-runner/__tests__/bench-circular-import.ts + * + * With CPU profiling: + * node --cpu-prof --cpu-prof-dir=./profiles --import tsx packages/vite/src/module-runner/__tests__/bench-circular-import.ts + */ + +import type { HotPayload } from '../../types/hmrPayload' +import type { + InvokeSendData, + ViteFetchResult, +} from '../../shared/invokeMethods' +import type { ModuleRunnerTransport } from '../../shared/moduleRunnerTransport' +import { ModuleRunner } from '../runner' +import { ESModulesEvaluator } from '../esmEvaluator' + +// --------------------------------------------------------------------------- +// Graph generation +// --------------------------------------------------------------------------- + +interface GraphConfig { + /** Number of modules */ + moduleCount: number + /** Average imports per module */ + avgImports: number + /** Fraction of modules involved in cycles (0..1) */ + cycleFraction: number +} + +interface SyntheticGraph { + /** Map of module id -> list of dependency ids */ + edges: Map + /** The entry module id */ + entry: string +} + +function moduleId(i: number): string { + return `/mod_${i}.js` +} + +/** + * Generate a synthetic module graph with configurable size, density, and cycles. + * + * Strategy: + * 1. Create a chain: mod_0 -> mod_1 -> ... -> mod_{n-1} (ensures all reachable) + * 2. Add random cross-edges to reach target avg imports + * 3. Add back-edges to create cycles for cycleFraction of modules + */ +function generateGraph(config: GraphConfig): SyntheticGraph { + const { moduleCount, avgImports, cycleFraction } = config + const edges = new Map() + + // Initialize all modules + for (let i = 0; i < moduleCount; i++) { + edges.set(moduleId(i), []) + } + + // 1. Chain: each module imports the next (ensures all reachable from entry) + for (let i = 0; i < moduleCount - 1; i++) { + edges.get(moduleId(i))!.push(moduleId(i + 1)) + } + + // 2. Random edges to reach target density (may include back-edges) + const targetTotalEdges = moduleCount * avgImports + const currentEdges = moduleCount - 1 + const extraEdges = Math.max(0, targetTotalEdges - currentEdges) + + // Use seeded PRNG for reproducibility + let seed = 42 + const rand = () => { + seed = (seed * 1664525 + 1013904223) & 0x7fffffff + return seed / 0x7fffffff + } + + for (let e = 0; e < extraEdges; e++) { + const from = Math.floor(rand() * moduleCount) + const to = Math.floor(rand() * moduleCount) + if (from !== to) { + const deps = edges.get(moduleId(from))! + const target = moduleId(to) + if (!deps.includes(target)) { + deps.push(target) + } + } + } + + // 3. Add cycles: back-edges from later modules to earlier ones + const cycleModules = Math.floor(moduleCount * cycleFraction) + for (let c = 0; c < cycleModules; c++) { + // Pick a module in the later half, add back-edge to earlier half + const from = Math.floor(moduleCount * 0.5 + rand() * moduleCount * 0.5) + const to = Math.floor(rand() * moduleCount * 0.5) + if (from < moduleCount && to < moduleCount && from !== to) { + const deps = edges.get(moduleId(from))! + const target = moduleId(to) + if (!deps.includes(target)) { + deps.push(target) + } + } + } + + return { edges, entry: moduleId(0) } +} + +// --------------------------------------------------------------------------- +// SSR code generation +// --------------------------------------------------------------------------- + +/** + * Generate SSR-transformed code for a module with the given dependencies. + * The code uses __vite_ssr_import__ and __vite_ssr_exportName__ which are + * the parameter names injected by ESModulesEvaluator's AsyncFunction wrapper. + */ +function generateSSRCode(deps: string[]): string { + const lines: string[] = [] + + // Import each dependency + deps.forEach((dep, i) => { + lines.push( + `const __vite_ssr_import_${i}__ = await __vite_ssr_import__("${dep}");`, + ) + }) + + // Export a value + lines.push( + `__vite_ssr_exportName__("value", () => { try { return value } catch {} });`, + ) + lines.push(`const value = ${deps.length};`) + + return lines.join('\n') +} + +// --------------------------------------------------------------------------- +// Mock transport +// --------------------------------------------------------------------------- + +function createMockTransport(graph: SyntheticGraph): ModuleRunnerTransport { + const moduleMap = new Map() + + for (const [id, deps] of graph.edges) { + moduleMap.set(id, { + code: generateSSRCode(deps), + file: id, + id, + url: id, + invalidate: false, + }) + } + + return { + async invoke(payload: HotPayload) { + const invokeData = payload.data as InvokeSendData + const { name, data } = invokeData + + if (name === 'fetchModule') { + const [id] = data as [string, string?, object?] + const mod = moduleMap.get(id) + if (mod) { + return { result: mod } + } + return { result: { externalize: id, type: 'builtin' as const } } + } + + if (name === 'getBuiltins') { + return { result: [] } + } + + return { result: null } + }, + } +} + +// --------------------------------------------------------------------------- +// Instrumentation (monkey-patch to count calls) +// --------------------------------------------------------------------------- + +interface CallCounts { + cachedRequest: number + isCircularImport: number + evaluatedCacheHits: number + promiseCacheHits: number +} + +function instrumentRunner(runner: ModuleRunner): CallCounts { + const counts: CallCounts = { + cachedRequest: 0, + isCircularImport: 0, + evaluatedCacheHits: 0, + promiseCacheHits: 0, + } + + // Patch cachedRequest + const origCachedRequest = (runner as any).cachedRequest.bind(runner) + ;(runner as any).cachedRequest = function ( + url: string, + mod: any, + callstack: string[] = [], + metadata?: any, + ) { + counts.cachedRequest++ + if (mod.evaluated && mod.promise) { + counts.evaluatedCacheHits++ + } else if (mod.promise) { + counts.promiseCacheHits++ + } + return origCachedRequest(url, mod, callstack, metadata) + } + + // Patch isCircularImport (counts include recursive calls, not just top-level) + const origIsCircularImport = (runner as any).isCircularImport.bind(runner) + ;(runner as any).isCircularImport = function (...args: any[]) { + counts.isCircularImport++ + return origIsCircularImport(...args) + } + + return counts +} + +// --------------------------------------------------------------------------- +// Benchmark runner +// --------------------------------------------------------------------------- + +interface BenchResult { + moduleCount: number + edgeCount: number + timeMs: number + counts: CallCounts +} + +async function runBenchmark(config: GraphConfig): Promise { + const graph = generateGraph(config) + + // Count total edges + let edgeCount = 0 + for (const deps of graph.edges.values()) { + edgeCount += deps.length + } + + const transport = createMockTransport(graph) + const runner = new ModuleRunner( + { + transport, + hmr: false, + sourcemapInterceptor: false, + }, + new ESModulesEvaluator(), + ) + + const counts = instrumentRunner(runner) + + const start = performance.now() + await runner.import(graph.entry) + const timeMs = performance.now() - start + + await runner.close() + + return { + moduleCount: config.moduleCount, + edgeCount, + timeMs, + counts, + } +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main() { + console.log('=== Vite isCircularImport Benchmark ===\n') + + const scales = [100, 500, 1000, 2000, 5000] + const results: BenchResult[] = [] + + // Warmup: single small run to trigger JIT compilation + await runBenchmark({ moduleCount: 100, avgImports: 3, cycleFraction: 0.05 }) + + for (const n of scales) { + const result = await runBenchmark({ + moduleCount: n, + avgImports: 3, + cycleFraction: 0.05, + }) + results.push(result) + + console.log(`N=${n}:`) + console.log(` modules: ${result.moduleCount}, edges: ${result.edgeCount}`) + console.log(` time: ${result.timeMs.toFixed(1)}ms`) + console.log(` cachedRequest calls: ${result.counts.cachedRequest}`) + console.log(` isCircularImport calls: ${result.counts.isCircularImport}`) + console.log( + ` evaluated cache hits (skipped circular checks): ${result.counts.evaluatedCacheHits}`, + ) + console.log(` promise cache hits: ${result.counts.promiseCacheHits}`) + console.log( + ` ratio (isCircularImport / modules): ${(result.counts.isCircularImport / result.moduleCount).toFixed(1)}x`, + ) + console.log() + } + + // Scaling analysis + console.log('=== Scaling Analysis ===') + for (let i = 1; i < results.length; i++) { + const prev = results[i - 1] + const curr = results[i] + const nRatio = curr.moduleCount / prev.moduleCount + const timeRatio = curr.timeMs / prev.timeMs + console.log( + ` N: ${prev.moduleCount} -> ${curr.moduleCount} (${nRatio.toFixed(1)}x), ` + + `time: ${prev.timeMs.toFixed(1)}ms -> ${curr.timeMs.toFixed(1)}ms (${timeRatio.toFixed(1)}x)` + + `${timeRatio > nRatio * 1.3 ? ' ← SUPERLINEAR' : ''}`, + ) + } +} + +main().catch(console.error) From bd20409ca3626f66bd2fae8650438ae1f54c66de Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 16 Feb 2026 16:38:35 +0900 Subject: [PATCH 3/3] chore: move benchmark script and convert it to use the actual modulerunner --- packages/vite/scripts/benchCircularImport.ts | 218 ++++++++++++ .../__tests__/bench-circular-import.ts | 322 ------------------ 2 files changed, 218 insertions(+), 322 deletions(-) create mode 100644 packages/vite/scripts/benchCircularImport.ts delete mode 100644 packages/vite/src/module-runner/__tests__/bench-circular-import.ts diff --git a/packages/vite/scripts/benchCircularImport.ts b/packages/vite/scripts/benchCircularImport.ts new file mode 100644 index 00000000000000..1b507822d49743 --- /dev/null +++ b/packages/vite/scripts/benchCircularImport.ts @@ -0,0 +1,218 @@ +/** + * Benchmark for the ModuleRunner + * + * Creates synthetic module graphs at various scales and measures the + * total time to evaluate all modules. + * + * Usage: + * node packages/vite/scripts/benchCircularImport.ts + * + * With CPU profiling: + * node --cpu-prof --cpu-prof-dir=./profiles packages/vite/scripts/benchCircularImport.ts + */ + +import { createServer, createServerModuleRunner } from 'vite' + +// --------------------------------------------------------------------------- +// Graph generation +// --------------------------------------------------------------------------- + +interface GraphConfig { + /** Number of modules */ + moduleCount: number + /** Average imports per module */ + avgImports: number + /** Fraction of modules involved in cycles (0..1) */ + cycleFraction: number +} + +interface SyntheticGraph { + /** Map of module id -> list of dependency ids */ + edges: Map + /** The entry module id */ + entry: string +} + +function moduleId(i: number): string { + return `/bench/mod_${i}.js` +} + +/** + * Generate a synthetic module graph with configurable size, density, and cycles. + * + * Strategy: + * 1. Create a chain: mod_0 -> mod_1 -> ... -> mod_{n-1} (ensures all reachable) + * 2. Add random cross-edges to reach target avg imports + * 3. Add back-edges to create cycles for cycleFraction of modules + */ +function generateGraph(config: GraphConfig): SyntheticGraph { + const { moduleCount, avgImports, cycleFraction } = config + const edges = new Map() + + // Initialize all modules + for (let i = 0; i < moduleCount; i++) { + edges.set(moduleId(i), []) + } + + // 1. Chain: each module imports the next (ensures all reachable from entry) + for (let i = 0; i < moduleCount - 1; i++) { + edges.get(moduleId(i))!.push(moduleId(i + 1)) + } + + // 2. Random edges to reach target density (may include back-edges) + const targetTotalEdges = moduleCount * avgImports + const currentEdges = moduleCount - 1 + const extraEdges = Math.max(0, targetTotalEdges - currentEdges) + + // Use seeded PRNG for reproducibility + let seed = 42 + const rand = () => { + seed = (seed * 1664525 + 1013904223) & 0x7fffffff + return seed / 0x7fffffff + } + + for (let e = 0; e < extraEdges; e++) { + const from = Math.floor(rand() * moduleCount) + const to = Math.floor(rand() * moduleCount) + if (from !== to) { + const deps = edges.get(moduleId(from))! + const target = moduleId(to) + if (!deps.includes(target)) { + deps.push(target) + } + } + } + + // 3. Add cycles: back-edges from later modules to earlier ones + const cycleModules = Math.floor(moduleCount * cycleFraction) + for (let c = 0; c < cycleModules; c++) { + // Pick a module in the later half, add back-edge to earlier half + const from = Math.floor(moduleCount * 0.5 + rand() * moduleCount * 0.5) + const to = Math.floor(rand() * moduleCount * 0.5) + if (from < moduleCount && to < moduleCount && from !== to) { + const deps = edges.get(moduleId(from))! + const target = moduleId(to) + if (!deps.includes(target)) { + deps.push(target) + } + } + } + + return { edges, entry: moduleId(0) } +} + +// --------------------------------------------------------------------------- +// Benchmark plugin (resolveId + load) +// --------------------------------------------------------------------------- + +const BENCH_PREFIX = '/bench/' + +function createBenchPlugin(graph: SyntheticGraph) { + return { + name: 'bench-circular-import', + + resolveId(id: string) { + if (id.startsWith(BENCH_PREFIX)) { + // Return a virtual module id (null-byte prefix prevents fs lookup) + return `\0${id}` + } + }, + + load(id: string) { + if (id.startsWith(`\0${BENCH_PREFIX}`)) { + const realId = id.slice(1) + const deps = graph.edges.get(realId) + if (deps) { + const lines = deps.map( + (dep, i) => `import { value as __dep_${i}__ } from "${dep}";`, + ) + lines.push(`export const value = ${deps.length};`) + return lines.join('\n') + } + } + }, + } +} + +// --------------------------------------------------------------------------- +// Benchmark runner +// --------------------------------------------------------------------------- + +interface BenchResult { + moduleCount: number + edgeCount: number + timeMs: number +} + +async function runBenchmark(config: GraphConfig): Promise { + const graph = generateGraph(config) + + let edgeCount = 0 + for (const deps of graph.edges.values()) { + edgeCount += deps.length + } + + const server = await createServer({ + configFile: false, + // eslint-disable-next-line n/no-unsupported-features/node-builtins + root: import.meta.dirname, + logLevel: 'error', + server: { + middlewareMode: true, + ws: false, + }, + optimizeDeps: { + noDiscovery: true, + include: [], + }, + plugins: [createBenchPlugin(graph)], + }) + + const runner = createServerModuleRunner(server.environments.ssr, { + hmr: false, + sourcemapInterceptor: false, + }) + + const start = performance.now() + await runner.import(graph.entry) + const timeMs = performance.now() - start + + await runner.close() + await server.close() + + return { + moduleCount: config.moduleCount, + edgeCount, + timeMs, + } +} + +// --------------------------------------------------------------------------- +// Main +// --------------------------------------------------------------------------- + +async function main() { + console.log('=== Vite isCircularImport Benchmark ===\n') + + const scales = [100, 500, 1000, 2000, 5000] + const results: BenchResult[] = [] + + // Warmup: single small run to trigger JIT compilation + await runBenchmark({ moduleCount: 100, avgImports: 3, cycleFraction: 0.05 }) + + for (const n of scales) { + const result = await runBenchmark({ + moduleCount: n, + avgImports: 3, + cycleFraction: 0.05, + }) + results.push(result) + + console.log(`N=${n}:`) + console.log(` modules: ${result.moduleCount}, edges: ${result.edgeCount}`) + console.log(` time: ${result.timeMs.toFixed(1)}ms`) + console.log() + } +} + +main().catch(console.error) diff --git a/packages/vite/src/module-runner/__tests__/bench-circular-import.ts b/packages/vite/src/module-runner/__tests__/bench-circular-import.ts deleted file mode 100644 index 35ba5647c62653..00000000000000 --- a/packages/vite/src/module-runner/__tests__/bench-circular-import.ts +++ /dev/null @@ -1,322 +0,0 @@ -/** - * Benchmark for isCircularImport performance in Vite's ModuleRunner. - * - * Creates synthetic module graphs at various scales and measures the - * total time to evaluate all modules via ModuleRunner.import(). - * - * Usage: - * node --import tsx packages/vite/src/module-runner/__tests__/bench-circular-import.ts - * - * With CPU profiling: - * node --cpu-prof --cpu-prof-dir=./profiles --import tsx packages/vite/src/module-runner/__tests__/bench-circular-import.ts - */ - -import type { HotPayload } from '../../types/hmrPayload' -import type { - InvokeSendData, - ViteFetchResult, -} from '../../shared/invokeMethods' -import type { ModuleRunnerTransport } from '../../shared/moduleRunnerTransport' -import { ModuleRunner } from '../runner' -import { ESModulesEvaluator } from '../esmEvaluator' - -// --------------------------------------------------------------------------- -// Graph generation -// --------------------------------------------------------------------------- - -interface GraphConfig { - /** Number of modules */ - moduleCount: number - /** Average imports per module */ - avgImports: number - /** Fraction of modules involved in cycles (0..1) */ - cycleFraction: number -} - -interface SyntheticGraph { - /** Map of module id -> list of dependency ids */ - edges: Map - /** The entry module id */ - entry: string -} - -function moduleId(i: number): string { - return `/mod_${i}.js` -} - -/** - * Generate a synthetic module graph with configurable size, density, and cycles. - * - * Strategy: - * 1. Create a chain: mod_0 -> mod_1 -> ... -> mod_{n-1} (ensures all reachable) - * 2. Add random cross-edges to reach target avg imports - * 3. Add back-edges to create cycles for cycleFraction of modules - */ -function generateGraph(config: GraphConfig): SyntheticGraph { - const { moduleCount, avgImports, cycleFraction } = config - const edges = new Map() - - // Initialize all modules - for (let i = 0; i < moduleCount; i++) { - edges.set(moduleId(i), []) - } - - // 1. Chain: each module imports the next (ensures all reachable from entry) - for (let i = 0; i < moduleCount - 1; i++) { - edges.get(moduleId(i))!.push(moduleId(i + 1)) - } - - // 2. Random edges to reach target density (may include back-edges) - const targetTotalEdges = moduleCount * avgImports - const currentEdges = moduleCount - 1 - const extraEdges = Math.max(0, targetTotalEdges - currentEdges) - - // Use seeded PRNG for reproducibility - let seed = 42 - const rand = () => { - seed = (seed * 1664525 + 1013904223) & 0x7fffffff - return seed / 0x7fffffff - } - - for (let e = 0; e < extraEdges; e++) { - const from = Math.floor(rand() * moduleCount) - const to = Math.floor(rand() * moduleCount) - if (from !== to) { - const deps = edges.get(moduleId(from))! - const target = moduleId(to) - if (!deps.includes(target)) { - deps.push(target) - } - } - } - - // 3. Add cycles: back-edges from later modules to earlier ones - const cycleModules = Math.floor(moduleCount * cycleFraction) - for (let c = 0; c < cycleModules; c++) { - // Pick a module in the later half, add back-edge to earlier half - const from = Math.floor(moduleCount * 0.5 + rand() * moduleCount * 0.5) - const to = Math.floor(rand() * moduleCount * 0.5) - if (from < moduleCount && to < moduleCount && from !== to) { - const deps = edges.get(moduleId(from))! - const target = moduleId(to) - if (!deps.includes(target)) { - deps.push(target) - } - } - } - - return { edges, entry: moduleId(0) } -} - -// --------------------------------------------------------------------------- -// SSR code generation -// --------------------------------------------------------------------------- - -/** - * Generate SSR-transformed code for a module with the given dependencies. - * The code uses __vite_ssr_import__ and __vite_ssr_exportName__ which are - * the parameter names injected by ESModulesEvaluator's AsyncFunction wrapper. - */ -function generateSSRCode(deps: string[]): string { - const lines: string[] = [] - - // Import each dependency - deps.forEach((dep, i) => { - lines.push( - `const __vite_ssr_import_${i}__ = await __vite_ssr_import__("${dep}");`, - ) - }) - - // Export a value - lines.push( - `__vite_ssr_exportName__("value", () => { try { return value } catch {} });`, - ) - lines.push(`const value = ${deps.length};`) - - return lines.join('\n') -} - -// --------------------------------------------------------------------------- -// Mock transport -// --------------------------------------------------------------------------- - -function createMockTransport(graph: SyntheticGraph): ModuleRunnerTransport { - const moduleMap = new Map() - - for (const [id, deps] of graph.edges) { - moduleMap.set(id, { - code: generateSSRCode(deps), - file: id, - id, - url: id, - invalidate: false, - }) - } - - return { - async invoke(payload: HotPayload) { - const invokeData = payload.data as InvokeSendData - const { name, data } = invokeData - - if (name === 'fetchModule') { - const [id] = data as [string, string?, object?] - const mod = moduleMap.get(id) - if (mod) { - return { result: mod } - } - return { result: { externalize: id, type: 'builtin' as const } } - } - - if (name === 'getBuiltins') { - return { result: [] } - } - - return { result: null } - }, - } -} - -// --------------------------------------------------------------------------- -// Instrumentation (monkey-patch to count calls) -// --------------------------------------------------------------------------- - -interface CallCounts { - cachedRequest: number - isCircularImport: number - evaluatedCacheHits: number - promiseCacheHits: number -} - -function instrumentRunner(runner: ModuleRunner): CallCounts { - const counts: CallCounts = { - cachedRequest: 0, - isCircularImport: 0, - evaluatedCacheHits: 0, - promiseCacheHits: 0, - } - - // Patch cachedRequest - const origCachedRequest = (runner as any).cachedRequest.bind(runner) - ;(runner as any).cachedRequest = function ( - url: string, - mod: any, - callstack: string[] = [], - metadata?: any, - ) { - counts.cachedRequest++ - if (mod.evaluated && mod.promise) { - counts.evaluatedCacheHits++ - } else if (mod.promise) { - counts.promiseCacheHits++ - } - return origCachedRequest(url, mod, callstack, metadata) - } - - // Patch isCircularImport (counts include recursive calls, not just top-level) - const origIsCircularImport = (runner as any).isCircularImport.bind(runner) - ;(runner as any).isCircularImport = function (...args: any[]) { - counts.isCircularImport++ - return origIsCircularImport(...args) - } - - return counts -} - -// --------------------------------------------------------------------------- -// Benchmark runner -// --------------------------------------------------------------------------- - -interface BenchResult { - moduleCount: number - edgeCount: number - timeMs: number - counts: CallCounts -} - -async function runBenchmark(config: GraphConfig): Promise { - const graph = generateGraph(config) - - // Count total edges - let edgeCount = 0 - for (const deps of graph.edges.values()) { - edgeCount += deps.length - } - - const transport = createMockTransport(graph) - const runner = new ModuleRunner( - { - transport, - hmr: false, - sourcemapInterceptor: false, - }, - new ESModulesEvaluator(), - ) - - const counts = instrumentRunner(runner) - - const start = performance.now() - await runner.import(graph.entry) - const timeMs = performance.now() - start - - await runner.close() - - return { - moduleCount: config.moduleCount, - edgeCount, - timeMs, - counts, - } -} - -// --------------------------------------------------------------------------- -// Main -// --------------------------------------------------------------------------- - -async function main() { - console.log('=== Vite isCircularImport Benchmark ===\n') - - const scales = [100, 500, 1000, 2000, 5000] - const results: BenchResult[] = [] - - // Warmup: single small run to trigger JIT compilation - await runBenchmark({ moduleCount: 100, avgImports: 3, cycleFraction: 0.05 }) - - for (const n of scales) { - const result = await runBenchmark({ - moduleCount: n, - avgImports: 3, - cycleFraction: 0.05, - }) - results.push(result) - - console.log(`N=${n}:`) - console.log(` modules: ${result.moduleCount}, edges: ${result.edgeCount}`) - console.log(` time: ${result.timeMs.toFixed(1)}ms`) - console.log(` cachedRequest calls: ${result.counts.cachedRequest}`) - console.log(` isCircularImport calls: ${result.counts.isCircularImport}`) - console.log( - ` evaluated cache hits (skipped circular checks): ${result.counts.evaluatedCacheHits}`, - ) - console.log(` promise cache hits: ${result.counts.promiseCacheHits}`) - console.log( - ` ratio (isCircularImport / modules): ${(result.counts.isCircularImport / result.moduleCount).toFixed(1)}x`, - ) - console.log() - } - - // Scaling analysis - console.log('=== Scaling Analysis ===') - for (let i = 1; i < results.length; i++) { - const prev = results[i - 1] - const curr = results[i] - const nRatio = curr.moduleCount / prev.moduleCount - const timeRatio = curr.timeMs / prev.timeMs - console.log( - ` N: ${prev.moduleCount} -> ${curr.moduleCount} (${nRatio.toFixed(1)}x), ` + - `time: ${prev.timeMs.toFixed(1)}ms -> ${curr.timeMs.toFixed(1)}ms (${timeRatio.toFixed(1)}x)` + - `${timeRatio > nRatio * 1.3 ? ' ← SUPERLINEAR' : ''}`, - ) - } -} - -main().catch(console.error)