import { NormalModule } from 'next/dist/compiled/webpack/webpack' import { Span } from '../../../trace' import type { webpack5 as webpack } from 'next/dist/compiled/webpack/webpack' const pluginName = 'ProfilingPlugin' export const spans = new WeakMap() const moduleSpansByCompilation = new WeakMap< webpack.Compilation, WeakMap >() export const webpackInvalidSpans = new WeakMap() export class ProfilingPlugin { compiler: any runWebpackSpan: Span constructor({ runWebpackSpan }: { runWebpackSpan: Span }) { this.runWebpackSpan = runWebpackSpan } apply(compiler: any) { this.traceTopLevelHooks(compiler) this.traceCompilationHooks(compiler) this.compiler = compiler } traceHookPair( spanName: string | (() => string), startHook: any, stopHook: any, { parentSpan, attrs, onStart, onStop, }: { parentSpan?: () => Span attrs?: any onStart?: (span: Span, ...params: any[]) => void onStop?: () => void } = {} ) { let span: Span | undefined startHook.tap( { name: pluginName, stage: -Infinity }, (...params: any[]) => { const name = typeof spanName === 'function' ? spanName() : spanName const attributes = attrs ? attrs(...params) : attrs span = parentSpan ? parentSpan().traceChild(name, attributes) : this.runWebpackSpan.traceChild(name, attributes) if (onStart) onStart(span, ...params) } ) stopHook.tap({ name: pluginName, stage: Infinity }, () => { // `stopHook` may be triggered when `startHook` has not in cases // where `stopHook` is used as the terminating event for more // than one pair of hooks. if (!span) { return } if (onStop) onStop() span.stop() }) } traceTopLevelHooks(compiler: any) { this.traceHookPair( 'webpack-compilation', compiler.hooks.compilation, compiler.hooks.afterCompile, { parentSpan: () => webpackInvalidSpans.get(compiler) || this.runWebpackSpan, attrs: () => ({ name: compiler.name }), onStart: (span, compilation) => { spans.set(compilation, span) spans.set(compiler, span) moduleSpansByCompilation.set(compilation, new WeakMap()) }, } ) if (compiler.options.mode === 'development') { this.traceHookPair( () => `webpack-invalidated-${compiler.name}`, compiler.hooks.invalid, compiler.hooks.done, { onStart: (span) => webpackInvalidSpans.set(compiler, span), onStop: () => webpackInvalidSpans.delete(compiler), attrs: (fileName: any) => ({ trigger: fileName || 'manual', }), } ) } } traceCompilationHooks(compiler: any) { this.traceHookPair('emit', compiler.hooks.emit, compiler.hooks.afterEmit, { parentSpan: () => webpackInvalidSpans.get(compiler) || this.runWebpackSpan, }) this.traceHookPair('make', compiler.hooks.make, compiler.hooks.finishMake, { parentSpan: () => webpackInvalidSpans.get(compiler) || this.runWebpackSpan, }) compiler.hooks.compilation.tap(pluginName, (compilation: any) => { compilation.hooks.buildModule.tap(pluginName, (module: any) => { const compilationSpan = spans.get(compilation) if (!compilationSpan) { return } const moduleType = (() => { if (!module.userRequest) { return '' } return module.userRequest.split('.').pop() })() const issuerModule = compilation?.moduleGraph?.getIssuer(module) let span: Span const moduleSpans = moduleSpansByCompilation.get(compilation) const spanName = `build-module${moduleType ? `-${moduleType}` : ''}` const issuerSpan: Span | undefined = issuerModule && moduleSpans?.get(issuerModule) if (issuerSpan) { span = issuerSpan.traceChild(spanName) } else { span = compilationSpan.traceChild(spanName) } span.setAttribute('name', module.userRequest) moduleSpans!.set(module, span) }) const moduleHooks = NormalModule.getCompilationHooks(compilation) // @ts-ignore TODO: remove ignore when using webpack 5 types moduleHooks.readResource.for(undefined).intercept({ register(tapInfo: any) { const fn = tapInfo.fn tapInfo.fn = (loaderContext: any, callback: any) => { const moduleSpan = loaderContext.currentTraceSpan.traceChild(`read-resource`) fn(loaderContext, (err: any, result: any) => { moduleSpan.stop() callback(err, result) }) } return tapInfo }, }) moduleHooks.loader.tap(pluginName, (loaderContext: any, module: any) => { const moduleSpan = moduleSpansByCompilation .get(compilation) ?.get(module) loaderContext.currentTraceSpan = moduleSpan }) compilation.hooks.succeedModule.tap(pluginName, (module: any) => { moduleSpansByCompilation?.get(compilation)?.get(module)?.stop() }) this.traceHookPair( 'webpack-compilation-seal', compilation.hooks.seal, compilation.hooks.afterSeal, { parentSpan: () => spans.get(compilation)! } ) compilation.hooks.addEntry.tap(pluginName, (entry: any) => { const compilationSpan = spans.get(compilation) if (!compilationSpan) { return } const addEntrySpan = compilationSpan.traceChild('add-entry') addEntrySpan.setAttribute('request', entry.request) spans.set(entry, addEntrySpan) }) compilation.hooks.succeedEntry.tap(pluginName, (entry: any) => { spans.get(entry)?.stop() }) this.traceHookPair( 'webpack-compilation-chunk-graph', compilation.hooks.beforeChunks, compilation.hooks.afterChunks, { parentSpan: () => spans.get(compilation) || spans.get(compiler)! } ) this.traceHookPair( 'webpack-compilation-optimize', compilation.hooks.optimize, compilation.hooks.reviveModules, { parentSpan: () => spans.get(compilation) || spans.get(compiler)! } ) this.traceHookPair( 'webpack-compilation-optimize-modules', compilation.hooks.optimizeModules, compilation.hooks.afterOptimizeModules, { parentSpan: () => spans.get(compilation) || spans.get(compiler)! } ) this.traceHookPair( 'webpack-compilation-optimize-chunks', compilation.hooks.optimizeChunks, compilation.hooks.afterOptimizeChunks, { parentSpan: () => spans.get(compilation) || spans.get(compiler)! } ) this.traceHookPair( 'webpack-compilation-optimize-tree', compilation.hooks.optimizeTree, compilation.hooks.afterOptimizeTree, { parentSpan: () => spans.get(compilation) || spans.get(compiler)! } ) this.traceHookPair( 'webpack-compilation-hash', compilation.hooks.beforeHash, compilation.hooks.afterHash, { parentSpan: () => spans.get(compilation) || spans.get(compiler)! } ) }) } }