Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improvements to webpack tracing, including hot-reload #21652

Merged
merged 14 commits into from
Jan 29, 2021
43 changes: 43 additions & 0 deletions packages/next/build/tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,49 @@ import api, { Span } from '@opentelemetry/api'

export const tracer = api.trace.getTracer('next', process.env.__NEXT_VERSION)

const compilerStacks = new WeakMap()

export function stackPush(compiler: any, spanName: string, attrs?: any): any {
Copy link
Contributor Author

@divmain divmain Jan 29, 2021

Choose a reason for hiding this comment

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

Using this approach allowed for easier association between parent and child. The error handling in stackPop down below also helped to identify when the start/stop tracing events were placed incorrectly.

let stack = compilerStacks.get(compiler)
let span

if (!stack) {
compilerStacks.set(compiler, (stack = []))
span = tracer.startSpan(spanName, attrs)
} else {
const parent = stack[stack.length - 1]
tracer.withSpan(parent, () => {
span = tracer.startSpan(spanName, attrs)
})
}

stack.push(span)
return span
}

export function stackPop(compiler: any, span: any) {
span.end()

let stack = compilerStacks.get(compiler)
if (!stack) {
console.warn(
'Attempted to pop from non-existent stack. Compiler reference must be bad.'
)
return
}
const poppedSpan = stack.pop()
if (poppedSpan !== span) {
stack.push(poppedSpan)
const spanIdx = stack.indexOf(span)
console.warn('Attempted to pop span that was not at top of stack.')
if (spanIdx !== -1) {
console.info(
`Span was found at index ${spanIdx} with stack size ${stack.length}`
)
}
}
}

export function traceFn<T extends (...args: unknown[]) => ReturnType<T>>(
span: Span,
fn: T
Expand Down
129 changes: 116 additions & 13 deletions packages/next/build/webpack/plugins/profiling-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { tracer } from '../../tracer'
import { tracer, stackPush, stackPop } from '../../tracer'
import { webpack, isWebpack5 } from 'next/dist/compiled/webpack/webpack'
import {
Span,
trace,
ProxyTracerProvider,
NoopTracerProvider,
} from '@opentelemetry/api'

const pluginName = 'ProfilingPlugin'

export const spans = new WeakMap()

function getNormalModuleLoaderHook(compilation: any) {
Expand All @@ -14,26 +19,85 @@ function getNormalModuleLoaderHook(compilation: any) {
return compilation.hooks.normalModuleLoader
}

function tracingIsEnabled() {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I found something that works! No need for a top-level span to be present for the plugin to activate.

Copy link
Member

Choose a reason for hiding this comment

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

Should this be moved to tracer.ts so that it can be reused in other places if needed?

const tracerProvider: any = trace.getTracerProvider()
if (tracerProvider instanceof ProxyTracerProvider) {
const proxyDelegate: any = tracerProvider.getDelegate()
return !(proxyDelegate instanceof NoopTracerProvider)
}
return false
}

export class ProfilingPlugin {
compiler: any

apply(compiler: any) {
// Only enabled when instrumentation is loaded
const currentSpan = tracer.getCurrentSpan()
if (!currentSpan || !currentSpan.isRecording()) {
// Only enable plugin when instrumentation is loaded
if (!tracingIsEnabled()) {
return
}
this.traceTopLevelHooks(compiler)
this.traceCompilationHooks(compiler)
this.compiler = compiler
}

compiler.hooks.compile.tap(pluginName, () => {
const span = tracer.startSpan('webpack-compile', {
attributes: { name: compiler.name },
})
spans.set(compiler, span)
traceHookPair(
spanName: string,
startHook: any,
stopHook: any,
attrs?: any,
onSetSpan?: (span: Span | undefined) => void
) {
let span: Span | undefined
startHook.tap(pluginName, () => {
span = stackPush(this.compiler, spanName, attrs)
onSetSpan?.(span)
})
stopHook.tap(pluginName, () => {
stackPop(this.compiler, span)
})
}

traceLoopedHook(spanName: string, startHook: any, stopHook: any) {
let span: Span | undefined
startHook.tap(pluginName, () => {
if (!span) {
span = stackPush(this.compiler, spanName)
}
})
compiler.hooks.done.tap(pluginName, () => {
spans.get(compiler).end()
stopHook.tap(pluginName, () => {
stackPop(this.compiler, span)
})
}

traceTopLevelHooks(compiler: any) {
this.traceHookPair(
'webpack-compile',
compiler.hooks.compile,
compiler.hooks.done,
{ attributes: { name: compiler.name } },
(span) => spans.set(compiler, span)
)
this.traceHookPair(
'webpack-prepare-env',
compiler.hooks.environment,
compiler.hooks.afterEnvironment
)
this.traceHookPair(
'webpack-invalidated',
compiler.hooks.invalid,
compiler.hooks.done
)
}

traceCompilationHooks(compiler: any) {
compiler.hooks.compilation.tap(pluginName, (compilation: any) => {
compilation.hooks.buildModule.tap(pluginName, (module: any) => {
tracer.withSpan(spans.get(compiler), () => {
const compilerSpan = spans.get(compiler)
if (!compilerSpan) {
return
}
tracer.withSpan(compilerSpan, () => {
const span = tracer.startSpan('build-module')
span.setAttribute('name', module.userRequest)
spans.set(module, span)
Expand All @@ -51,6 +115,45 @@ export class ProfilingPlugin {
compilation.hooks.succeedModule.tap(pluginName, (module: any) => {
spans.get(module).end()
})

if (isWebpack5) {
this.traceHookPair(
'webpack-compilation',
compilation.hooks.beforeCompile,
compilation.hooks.afterCompile
)
}

this.traceHookPair(
'webpack-compilation-chunk-graph',
compilation.hooks.beforeChunks,
compilation.hooks.afterChunks
)
this.traceHookPair(
'webpack-compilation-optimize',
compilation.hooks.optimize,
compilation.hooks.reviveModules
)
this.traceLoopedHook(
'webpack-compilation-optimize-modules',
compilation.hooks.optimizeModules,
compilation.hooks.afterOptimizeModules
)
this.traceLoopedHook(
'webpack-compilation-optimize-chunks',
compilation.hooks.optimizeChunks,
compilation.hooks.afterOptimizeChunks
)
this.traceHookPair(
'webpack-compilation-optimize-tree',
compilation.hooks.optimizeTree,
compilation.hooks.afterOptimizeTree
)
this.traceHookPair(
'webpack-compilation-hash',
compilation.hooks.beforeHash,
compilation.hooks.afterHash
)
})
}
}