256 lines
6.7 KiB
TypeScript
256 lines
6.7 KiB
TypeScript
|
// Copy of https://github.com/webpack/webpack/blob/master/lib/debug/ProfilingPlugin.js
|
||
|
// License:
|
||
|
/*
|
||
|
Copyright JS Foundation and other contributors
|
||
|
|
||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||
|
a copy of this software and associated documentation files (the
|
||
|
'Software'), to deal in the Software without restriction, including
|
||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||
|
permit persons to whom the Software is furnished to do so, subject to
|
||
|
the following conditions:
|
||
|
|
||
|
The above copyright notice and this permission notice shall be
|
||
|
included in all copies or substantial portions of the Software.
|
||
|
|
||
|
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||
|
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||
|
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||
|
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||
|
*/
|
||
|
|
||
|
// Includes fix https://github.com/webpack/webpack/pull/9566
|
||
|
// Includes support for custom tracer that can capture across multiple builds
|
||
|
const pluginName = 'ProfilingPlugin'
|
||
|
|
||
|
export class ProfilingPlugin {
|
||
|
tracer: any
|
||
|
/**
|
||
|
* @param {ProfilingPluginOptions=} opts options object
|
||
|
*/
|
||
|
constructor(opts: { tracer: any }) {
|
||
|
this.tracer = opts.tracer
|
||
|
}
|
||
|
|
||
|
apply(compiler: any) {
|
||
|
const tracer = this.tracer
|
||
|
|
||
|
// Compiler Hooks
|
||
|
Object.keys(compiler.hooks).forEach(hookName => {
|
||
|
compiler.hooks[hookName].intercept(
|
||
|
makeInterceptorFor('Compiler', tracer)(hookName)
|
||
|
)
|
||
|
})
|
||
|
|
||
|
Object.keys(compiler.resolverFactory.hooks).forEach(hookName => {
|
||
|
compiler.resolverFactory.hooks[hookName].intercept(
|
||
|
makeInterceptorFor('Resolver', tracer)(hookName)
|
||
|
)
|
||
|
})
|
||
|
|
||
|
compiler.hooks.compilation.tap(
|
||
|
pluginName,
|
||
|
(
|
||
|
compilation: any,
|
||
|
{ normalModuleFactory, contextModuleFactory }: any
|
||
|
) => {
|
||
|
interceptAllHooksFor(compilation, tracer, 'Compilation')
|
||
|
interceptAllHooksFor(
|
||
|
normalModuleFactory,
|
||
|
tracer,
|
||
|
'Normal Module Factory'
|
||
|
)
|
||
|
interceptAllHooksFor(
|
||
|
contextModuleFactory,
|
||
|
tracer,
|
||
|
'Context Module Factory'
|
||
|
)
|
||
|
interceptAllParserHooks(normalModuleFactory, tracer)
|
||
|
interceptTemplateInstancesFrom(compilation, tracer)
|
||
|
}
|
||
|
)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const interceptTemplateInstancesFrom = (compilation: any, tracer: any) => {
|
||
|
const {
|
||
|
mainTemplate,
|
||
|
chunkTemplate,
|
||
|
hotUpdateChunkTemplate,
|
||
|
moduleTemplates,
|
||
|
} = compilation
|
||
|
|
||
|
const { javascript, webassembly } = moduleTemplates
|
||
|
;[
|
||
|
{
|
||
|
instance: mainTemplate,
|
||
|
name: 'MainTemplate',
|
||
|
},
|
||
|
{
|
||
|
instance: chunkTemplate,
|
||
|
name: 'ChunkTemplate',
|
||
|
},
|
||
|
{
|
||
|
instance: hotUpdateChunkTemplate,
|
||
|
name: 'HotUpdateChunkTemplate',
|
||
|
},
|
||
|
{
|
||
|
instance: javascript,
|
||
|
name: 'JavaScriptModuleTemplate',
|
||
|
},
|
||
|
{
|
||
|
instance: webassembly,
|
||
|
name: 'WebAssemblyModuleTemplate',
|
||
|
},
|
||
|
].forEach(templateObject => {
|
||
|
Object.keys(templateObject.instance.hooks).forEach(hookName => {
|
||
|
templateObject.instance.hooks[hookName].intercept(
|
||
|
makeInterceptorFor(templateObject.name, tracer)(hookName)
|
||
|
)
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
|
||
|
const interceptAllHooksFor = (instance: any, tracer: any, logLabel: any) => {
|
||
|
if (Reflect.has(instance, 'hooks')) {
|
||
|
Object.keys(instance.hooks).forEach(hookName => {
|
||
|
instance.hooks[hookName].intercept(
|
||
|
makeInterceptorFor(logLabel, tracer)(hookName)
|
||
|
)
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const interceptAllParserHooks = (moduleFactory: any, tracer: any) => {
|
||
|
const moduleTypes = [
|
||
|
'javascript/auto',
|
||
|
'javascript/dynamic',
|
||
|
'javascript/esm',
|
||
|
'json',
|
||
|
'webassembly/experimental',
|
||
|
]
|
||
|
|
||
|
moduleTypes.forEach(moduleType => {
|
||
|
moduleFactory.hooks.parser
|
||
|
.for(moduleType)
|
||
|
.tap('ProfilingPlugin', (parser: any, parserOpts: any) => {
|
||
|
interceptAllHooksFor(parser, tracer, 'Parser')
|
||
|
})
|
||
|
})
|
||
|
}
|
||
|
|
||
|
const makeInterceptorFor = (instance: any, tracer: any) => (hookName: any) => ({
|
||
|
register: ({ name, type, context, fn }: any) => {
|
||
|
const newFn = makeNewProfiledTapFn(hookName, tracer, {
|
||
|
name,
|
||
|
type,
|
||
|
fn,
|
||
|
})
|
||
|
return {
|
||
|
name,
|
||
|
type,
|
||
|
context,
|
||
|
fn: newFn,
|
||
|
}
|
||
|
},
|
||
|
})
|
||
|
|
||
|
// TODO improve typing
|
||
|
/** @typedef {(...args: TODO[]) => void | Promise<TODO>} PluginFunction */
|
||
|
|
||
|
/**
|
||
|
* @param {string} hookName Name of the hook to profile.
|
||
|
* @param {Trace} tracer The trace object.
|
||
|
* @param {object} options Options for the profiled fn.
|
||
|
* @param {string} options.name Plugin name
|
||
|
* @param {string} options.type Plugin type (sync | async | promise)
|
||
|
* @param {PluginFunction} options.fn Plugin function
|
||
|
* @returns {PluginFunction} Chainable hooked function.
|
||
|
*/
|
||
|
const makeNewProfiledTapFn = (
|
||
|
hookName: any,
|
||
|
tracer: any,
|
||
|
{ name, type, fn }: any
|
||
|
) => {
|
||
|
const defaultCategory = ['blink.user_timing']
|
||
|
|
||
|
switch (type) {
|
||
|
case 'promise':
|
||
|
return (...args: any) => {
|
||
|
const id = ++tracer.counter
|
||
|
tracer.trace.begin({
|
||
|
name,
|
||
|
id,
|
||
|
cat: defaultCategory,
|
||
|
})
|
||
|
const promise = /** @type {Promise<*>} */ fn(...args)
|
||
|
return promise.then((r: any) => {
|
||
|
tracer.trace.end({
|
||
|
name,
|
||
|
id,
|
||
|
cat: defaultCategory,
|
||
|
})
|
||
|
return r
|
||
|
})
|
||
|
}
|
||
|
case 'async':
|
||
|
return (...args: any) => {
|
||
|
const id = ++tracer.counter
|
||
|
tracer.trace.begin({
|
||
|
name,
|
||
|
id,
|
||
|
cat: defaultCategory,
|
||
|
})
|
||
|
const callback = args.pop()
|
||
|
/* eslint-disable */
|
||
|
fn(...args, (...r: any) => {
|
||
|
tracer.trace.end({
|
||
|
name,
|
||
|
id,
|
||
|
cat: defaultCategory,
|
||
|
})
|
||
|
callback(...r)
|
||
|
})
|
||
|
/* eslint-enable */
|
||
|
}
|
||
|
case 'sync':
|
||
|
return (...args: any) => {
|
||
|
const id = ++tracer.counter
|
||
|
// Do not instrument ourself due to the CPU
|
||
|
// profile needing to be the last event in the trace.
|
||
|
if (name === pluginName) {
|
||
|
return fn(...args)
|
||
|
}
|
||
|
|
||
|
tracer.trace.begin({
|
||
|
name,
|
||
|
id,
|
||
|
cat: defaultCategory,
|
||
|
})
|
||
|
let r
|
||
|
try {
|
||
|
r = fn(...args)
|
||
|
} catch (error) {
|
||
|
tracer.trace.end({
|
||
|
name,
|
||
|
id,
|
||
|
cat: defaultCategory,
|
||
|
})
|
||
|
throw error
|
||
|
}
|
||
|
tracer.trace.end({
|
||
|
name,
|
||
|
id,
|
||
|
cat: defaultCategory,
|
||
|
})
|
||
|
return r
|
||
|
}
|
||
|
default:
|
||
|
break
|
||
|
}
|
||
|
}
|