Skip to content

Commit 791cfdb

Browse files
sokragaojude
authored andcommitted
Turbopack: use raw_read_dir to create fewer FileSystemPaths (#75237)
### What? * use raw_read_dir to avoid creating many FileSystemPaths * use raw_read_dir in resolving
1 parent 6cbaddf commit 791cfdb

File tree

6 files changed

+453
-1
lines changed

6 files changed

+453
-1
lines changed

packages/next/errors.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -626,5 +626,12 @@
626626
"625": "Server Actions are not supported with static export.\\nRead more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features",
627627
"626": "Intercepting routes are not supported with static export.\\nRead more: https://nextjs.org/docs/app/building-your-application/deploying/static-exports#unsupported-features",
628628
"627": "cacheLife() is only available with the experimental.useCache config.",
629-
"628": "cacheTag() is only available with the experimental.useCache config."
629+
"628": "cacheTag() is only available with the experimental.useCache config.",
630+
"629": "[SlowModuleDetectionPlugin] Cannot record additional build times: The analysis has already been finalized.",
631+
"630": "[SlowModuleDetectionPlugin] Cannot regenerate dependency graph: The graph has already been generated.",
632+
"631": "[SlowModuleDetectionPlugin] Circular dependency detected in module graph.",
633+
"632": "[SlowModuleDetectionPlugin] Cannot generate performance report: The module analysis has not been finalized.",
634+
"633": "Unable to identify module: The module is missing a required debugId. This may indicate a problem with webpack's internal module tracking.",
635+
"634": "[SlowModuleDetectionPlugin] Module analyzer initialization error: The analyzer was not properly initialized.",
636+
"635": "[SlowModuleDetectionPlugin] Module build timing error: Unable to find the start time for a module build."
630637
}

packages/next/src/build/webpack-config.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1944,6 +1944,14 @@ export default async function getBaseWebpackConfig(
19441944
new (
19451945
require('./webpack/plugins/telemetry-plugin/telemetry-plugin') as typeof import('./webpack/plugins/telemetry-plugin/telemetry-plugin')
19461946
).TelemetryPlugin(new Map()),
1947+
dev &&
1948+
config.experimental?.slowModuleDetectionWebpack &&
1949+
new (
1950+
require('./webpack/plugins/slow-module-detection-plugin') as typeof import('./webpack/plugins/slow-module-detection-plugin')
1951+
).default({
1952+
compilerType,
1953+
...config.experimental.slowModuleDetectionWebpack,
1954+
}),
19471955
].filter(Boolean as any as ExcludesFalse),
19481956
}
19491957

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
import type { Compiler, Module, Compilation } from 'webpack'
2+
import type { CompilerNameValues } from '../../../shared/lib/constants'
3+
4+
/**
5+
* Plugin that detects and reports modules that take a long time to build.
6+
* Helps identify performance bottlenecks in the build process by:
7+
* 1. Tracking build time for each module
8+
* 2. Building a dependency tree of slow modules
9+
* 3. Generating a visual report with timing information
10+
*/
11+
12+
// Configuration constants
13+
const PLUGIN_NAME = 'SlowModuleDetectionPlugin'
14+
15+
// Console output styling - using ANSI escape codes for colors
16+
const ConsoleColors = {
17+
BOLD_BLUE: '\x1b[1;34m',
18+
BRIGHT_YELLOW: '\x1b[93m',
19+
BOLD_GREEN: '\x1b[1;32m',
20+
RESET: '\x1b[0m',
21+
} as const
22+
23+
// Tree visualization characters for the dependency tree output
24+
const TreeSymbols = {
25+
VERTICAL_LINE: '│ ', // Used for showing depth levels
26+
BRANCH: '├─ ', // Used for showing module connections
27+
} as const
28+
29+
// Matches node_modules paths, including pnpm-style paths (.pnpm folder)
30+
const NODE_MODULES_PATH_PATTERN = /node_modules(?:\/\.pnpm)?\/(.*)/
31+
32+
interface ModuleBuildTimeAnalyzerOptions {
33+
compilerType: CompilerNameValues
34+
slowModuleThresholdMs?: number
35+
pathTruncationLength?: number
36+
}
37+
38+
interface ModuleNode {
39+
module: Module
40+
buildDuration: number // Time taken to build this module in milliseconds
41+
}
42+
43+
/**
44+
* Gets a unique identifier for a module based on its debugId.
45+
* The debugId is a webpack-internal identifier that's guaranteed unique per module.
46+
*/
47+
const getModuleIdentifier = (module: Module): string => {
48+
const debugId = module.debugId
49+
if (!debugId) {
50+
throw new Error(
51+
"Unable to identify module: The module is missing a required debugId. This may indicate a problem with webpack's internal module tracking."
52+
)
53+
}
54+
return String(debugId)
55+
}
56+
57+
/**
58+
* Extracts a clean, readable module name from its resource path.
59+
* Handles both project files and node_modules:
60+
* - Project files: Converts absolute paths to project-relative paths
61+
* - node_modules: Simplifies to just the package path
62+
*/
63+
const getModuleDisplayName = (module: Module): string | undefined => {
64+
const resourcePath = (module as any).resource
65+
if (!resourcePath) {
66+
return undefined
67+
}
68+
69+
// Convert absolute paths to project-relative paths for readability
70+
let displayPath = resourcePath.replace(process.cwd(), '.')
71+
72+
// Simplify node_modules paths to just show the package path
73+
const nodeModulesMatch = displayPath.match(NODE_MODULES_PATH_PATTERN)
74+
if (nodeModulesMatch) {
75+
return nodeModulesMatch[1]
76+
}
77+
78+
return displayPath
79+
}
80+
81+
/**
82+
* Creates a new module node with initial build duration of 0
83+
*/
84+
const createModuleNode = (module: Module): ModuleNode => {
85+
return {
86+
module,
87+
buildDuration: 0,
88+
}
89+
}
90+
91+
/**
92+
* Tracks and analyzes module build times to generate a dependency tree report.
93+
* The analyzer maintains several data structures:
94+
* - pendingModules: Modules waiting to be processed
95+
* - moduleNodes: Map of all module nodes by ID
96+
* - moduleParents/Children: Bidirectional relationships between modules
97+
*/
98+
class ModuleBuildTimeAnalyzer {
99+
private pendingModules: [Module, number, Compilation][] = []
100+
private moduleNodes = new Map<string, ModuleNode>()
101+
private moduleParents = new Map<ModuleNode, ModuleNode>()
102+
private moduleChildren = new Map<ModuleNode, Map<string, ModuleNode>>()
103+
private isFinalized = false
104+
private slowModuleThresholdMs: number
105+
private pathTruncationLength: number
106+
107+
constructor(private options: ModuleBuildTimeAnalyzerOptions) {
108+
this.slowModuleThresholdMs = options.slowModuleThresholdMs ?? 500
109+
this.pathTruncationLength = options.pathTruncationLength ?? 100
110+
}
111+
112+
recordModuleBuildTime(
113+
module: Module,
114+
duration: number,
115+
compilation: Compilation
116+
) {
117+
if (this.isFinalized) {
118+
throw new Error(
119+
'[SlowModuleDetectionPlugin] Cannot record additional build times: The analysis has already been finalized.'
120+
)
121+
}
122+
if (duration < this.slowModuleThresholdMs) {
123+
return // Skip fast modules to reduce noise
124+
}
125+
126+
this.pendingModules.push([module, duration, compilation])
127+
}
128+
129+
private generateDependencyGraph() {
130+
if (this.isFinalized) {
131+
throw new Error(
132+
'[SlowModuleDetectionPlugin] Cannot regenerate dependency graph: The graph has already been generated.'
133+
)
134+
}
135+
136+
for (const [module, duration, compilation] of this.pendingModules) {
137+
// Build dependency chain by walking up the module graph
138+
const chain: Module[] = []
139+
const { moduleGraph } = compilation
140+
const visitedModules = new Set<Module>()
141+
let currentModule = module
142+
143+
chain.push(currentModule)
144+
visitedModules.add(currentModule)
145+
146+
while (true) {
147+
const issuerModule = moduleGraph.getIssuer(currentModule)
148+
if (!issuerModule) break
149+
if (visitedModules.has(issuerModule)) {
150+
throw new Error(
151+
'[SlowModuleDetectionPlugin] Circular dependency detected in module graph.'
152+
)
153+
}
154+
chain.push(issuerModule)
155+
visitedModules.add(issuerModule)
156+
currentModule = issuerModule
157+
}
158+
159+
const moduleNodes: ModuleNode[] = []
160+
161+
// Create or reuse nodes for each module in the chain
162+
for (const mod of chain) {
163+
let node = this.moduleNodes.get(getModuleIdentifier(mod))
164+
if (!node) {
165+
node = createModuleNode(mod)
166+
this.moduleNodes.set(getModuleIdentifier(mod), node)
167+
}
168+
moduleNodes.push(node)
169+
}
170+
171+
// Add the build time to the module that triggered the build
172+
if (moduleNodes.length) {
173+
moduleNodes[0].buildDuration += duration
174+
}
175+
176+
// Build the parent-child relationships for the dependency tree
177+
for (let i = 0; i < moduleNodes.length - 1; i++) {
178+
const child = moduleNodes[i]
179+
const parent = moduleNodes[i + 1]
180+
181+
this.moduleParents.set(child, parent)
182+
183+
let parentChildren = this.moduleChildren.get(parent)
184+
if (!parentChildren) {
185+
parentChildren = new Map()
186+
this.moduleChildren.set(parent, parentChildren)
187+
}
188+
parentChildren.set(getModuleIdentifier(child.module), child)
189+
}
190+
}
191+
192+
this.isFinalized = true
193+
}
194+
195+
/**
196+
* Generates a visual tree report of slow modules and their relationships.
197+
* The tree shows:
198+
* - Module paths (truncated if too long)
199+
* - Build durations (in yellow)
200+
* - Module relationships (using tree symbols)
201+
*/
202+
generateReport() {
203+
this.generateDependencyGraph()
204+
205+
if (!this.isFinalized) {
206+
throw new Error(
207+
'[SlowModuleDetectionPlugin] Cannot generate performance report: The module analysis has not been finalized.'
208+
)
209+
}
210+
211+
// Find root modules (those with no parents)
212+
const rootModules = [...this.moduleNodes.values()].filter(
213+
(node) => !this.moduleParents.has(node)
214+
)
215+
216+
// Helper to truncate long paths for better readability
217+
const truncatePath = (
218+
path: string,
219+
maxLength = this.pathTruncationLength
220+
): string => {
221+
if (path.length <= maxLength) return path
222+
const startSegment = path.slice(0, maxLength / 3)
223+
const endSegment = path.slice((-maxLength * 2) / 3)
224+
return `${startSegment}...${endSegment}`
225+
}
226+
227+
// Helper to format duration with consistent padding
228+
const formatDuration = (ms: number): string => {
229+
return `${ms}ms`.padStart(5)
230+
}
231+
232+
// Recursively format a module and its children
233+
const formatModuleNode = (node: ModuleNode, depth: number): string => {
234+
const moduleName = getModuleDisplayName(node.module) || ''
235+
236+
if (!moduleName) {
237+
return formatChildModules(node, depth)
238+
}
239+
240+
const truncatedName = truncatePath(moduleName)
241+
const indentation = TreeSymbols.VERTICAL_LINE.repeat(depth)
242+
243+
const duration =
244+
node.buildDuration > 0
245+
? `(${ConsoleColors.BRIGHT_YELLOW}${formatDuration(node.buildDuration)}${ConsoleColors.RESET})`
246+
: ''
247+
const nameWithStyle = `${ConsoleColors.BOLD_BLUE}${truncatedName}${ConsoleColors.RESET}`
248+
const line = `${indentation}${TreeSymbols.BRANCH}${nameWithStyle} ${duration}`
249+
250+
return ` ${line}\n${formatChildModules(node, depth + 1)}`
251+
}
252+
253+
// Format all children of a module
254+
const formatChildModules = (node: ModuleNode, depth: number): string => {
255+
const children = this.moduleChildren.get(node)
256+
if (!children) return ''
257+
258+
return [...children]
259+
.map(([_, child]) => formatModuleNode(child, depth))
260+
.join('')
261+
}
262+
263+
// Build the complete report
264+
let report = ''
265+
for (const root of rootModules) {
266+
report += formatModuleNode(root, 0)
267+
}
268+
269+
// Only output if there are slow modules to report
270+
if (report) {
271+
console.log(
272+
`${ConsoleColors.BOLD_GREEN}🐌 Detected slow modules while compiling ${this.options.compilerType}:${ConsoleColors.RESET}`
273+
)
274+
console.log(report)
275+
}
276+
}
277+
}
278+
279+
/**
280+
* Webpack plugin that analyzes and reports modules that are slow to build.
281+
* Hooks into webpack's compilation process to:
282+
* 1. Track when module builds start
283+
* 2. Record build durations when modules complete
284+
* 3. Generate a report when compilation is done
285+
*/
286+
export default class SlowModuleDetectionPlugin {
287+
private analyzer: ModuleBuildTimeAnalyzer | null = null
288+
private moduleBuildStartTimes = new WeakMap<Module, number>()
289+
290+
constructor(private options: ModuleBuildTimeAnalyzerOptions) {}
291+
292+
apply = (compiler: Compiler) => {
293+
compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
294+
this.analyzer = new ModuleBuildTimeAnalyzer(this.options)
295+
296+
// Record start time when module build begins
297+
compilation.hooks.buildModule.tap(PLUGIN_NAME, (module) => {
298+
this.moduleBuildStartTimes.set(module, Date.now())
299+
})
300+
301+
// Calculate and record duration when module build succeeds
302+
compilation.hooks.succeedModule.tap(PLUGIN_NAME, (module) => {
303+
const startTime = this.moduleBuildStartTimes.get(module)
304+
if (!startTime) {
305+
throw new Error(
306+
'[SlowModuleDetectionPlugin] Module build timing error: Unable to find the start time for a module build.'
307+
)
308+
}
309+
if (!this.analyzer) {
310+
throw new Error(
311+
'[SlowModuleDetectionPlugin] Module analyzer initialization error: The analyzer was not properly initialized.'
312+
)
313+
}
314+
this.analyzer.recordModuleBuildTime(
315+
module,
316+
Date.now() - startTime,
317+
compilation
318+
)
319+
})
320+
})
321+
322+
// Generate the report when compilation is complete
323+
compiler.hooks.done.tap(PLUGIN_NAME, () => {
324+
if (!this.analyzer) {
325+
return // No modules were built
326+
}
327+
this.analyzer.generateReport()
328+
})
329+
}
330+
}

packages/next/src/server/config-schema.ts

+6
Original file line numberDiff line numberDiff line change
@@ -444,6 +444,12 @@ export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
444444
streamingMetadata: z.boolean().optional(),
445445
htmlLimitedBots: z.instanceof(RegExp).optional(),
446446
useCache: z.boolean().optional(),
447+
slowModuleDetectionWebpack: z
448+
.object({
449+
slowModuleThresholdMs: z.number().optional(),
450+
pathTruncationLength: z.number().optional(),
451+
})
452+
.optional(),
447453
})
448454
.optional(),
449455
exportPathMap: z

0 commit comments

Comments
 (0)