webpack
Version:
Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.
687 lines (626 loc) • 18.3 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
const { Tracer } = require("chrome-trace-event");
const { dirname, mkdirpSync } = require("../fs/fs");
const {
CSS_MODULES,
JAVASCRIPT_MODULES,
JSON_MODULE_TYPE,
WEBASSEMBLY_MODULES
} = require("../module/ModuleTypeConstants");
const { fileUrlToPath } = require("../util/identifier");
/** @import { Session } from "inspector" */
/** @import { FullTap } from "tapable" */
/** @import Compilation from "../Compilation" */
/** @import Compiler from "../Compiler" */
/** @import NormalModuleFactory from "../module/NormalModuleFactory" */
/** @import ResolverFactory from "../resolve/ResolverFactory" */
/** @import { IntermediateFileSystem } from "../fs/fs" */
/**
* @inline
* @absolutePath true
* @typedef {string} AbsolutePath
*/
/**
* @schema plugins/debug/ProfilingPlugin
* @typedef {object} ProfilingPluginOptions
* @property {AbsolutePath=} outputPath Path to the output file e.g. `path.resolve(__dirname, 'profiling/events.json')`. Defaults to `events.json`.
*/
/** @import { FakeHookMarker } from "../util/deprecation" */
/**
* Defines the hook interceptor type used by this module.
* @template T, R
* @typedef {import("tapable").HookInterceptor<T, R>} HookInterceptor
*/
/** @typedef {{ Session: typeof import("inspector").Session }} Inspector */
/** @type {Inspector | undefined} */
let inspector;
try {
// eslint-disable-next-line n/no-unsupported-features/node-builtins
inspector = require("inspector");
} catch (_err) {
// eslint-disable-next-line no-console
console.log("Unable to CPU profile in < node 8.0");
}
class Profiler {
/**
* Creates an instance of Profiler.
* @param {Inspector} inspector inspector
*/
constructor(inspector) {
/** @type {undefined | Session} */
this.session = undefined;
/** @type {Inspector} */
this.inspector = inspector;
/** @type {number} */
this._startTime = 0;
}
hasSession() {
return this.session !== undefined;
}
startProfiling() {
if (this.inspector === undefined) {
return Promise.resolve();
}
try {
this.session = new /** @type {Inspector} */ (inspector).Session();
this.session.connect();
} catch (_) {
this.session = undefined;
return Promise.resolve();
}
const hrtime = process.hrtime();
this._startTime = hrtime[0] * 1000000 + Math.round(hrtime[1] / 1000);
return Promise.all([
this.sendCommand("Profiler.setSamplingInterval", {
interval: 100
}),
this.sendCommand("Profiler.enable"),
this.sendCommand("Profiler.start")
]);
}
/**
* Returns promise for the result.
* @param {string} method method name
* @param {import("inspector").Profiler.SetSamplingIntervalParameterType=} params params
* @returns {Promise<Partial<import("inspector").Profiler.StopReturnType> | void>} Promise for the result, `{}` for commands without one
*/
sendCommand(method, params) {
if (this.hasSession()) {
return new Promise((res, rej) => {
/** @type {Session} */
(this.session).post(method, params, (err, params) => {
if (err !== null) {
rej(err);
} else {
res(params);
}
});
});
}
return Promise.resolve();
}
destroy() {
if (this.hasSession()) {
/** @type {Session} */
(this.session).disconnect();
}
return Promise.resolve();
}
/**
* Returns }>} profile result.
* @returns {Promise<{ profile: { startTime: number, endTime: number } }>} profile result
*/
stopProfiling() {
return /** @type {Promise<import("inspector").Profiler.StopReturnType>} */ (
this.sendCommand("Profiler.stop")
).then(({ profile }) => {
const hrtime = process.hrtime();
const endTime = hrtime[0] * 1000000 + Math.round(hrtime[1] / 1000);
// Avoid coverage problems due indirect changes
/* istanbul ignore next */
if (profile.startTime < this._startTime || profile.endTime > endTime) {
// `process.hrtime` and the inspector timestamps are each relative to an
// unknown point in time, with no guarantee it is the same point, so the
// two are rescaled onto one another here.
const duration = profile.endTime - profile.startTime;
const ownDuration = endTime - this._startTime;
const untracked = Math.max(0, ownDuration - duration);
profile.startTime = this._startTime + untracked / 2;
profile.endTime = endTime - untracked / 2;
}
return { profile };
});
}
}
/**
* an object that wraps Tracer and Profiler with a counter
* @typedef {object} Trace
* @property {Tracer} trace instance of Tracer
* @property {number} counter Counter
* @property {Profiler} profiler instance of Profiler
* @property {(callback: (err?: null | Error) => void) => void} end the end function
*/
/**
* Creates a trace from the provided f.
* @param {IntermediateFileSystem} fs filesystem used for output
* @param {string} outputPath The location where to write the log.
* @returns {Trace} The trace object
*/
const createTrace = (fs, outputPath) => {
const trace = new Tracer();
const profiler = new Profiler(/** @type {Inspector} */ (inspector));
if (/\/|\\/.test(outputPath)) {
const dirPath = dirname(fs, outputPath);
mkdirpSync(fs, dirPath);
}
const fsStream = fs.createWriteStream(outputPath);
let counter = 0;
trace.pipe(fsStream);
// These are critical events that need to be inserted so that tools like
// chrome dev tools can load the profile.
trace.instantEvent({
name: "TracingStartedInPage",
id: ++counter,
cat: ["disabled-by-default-devtools.timeline"],
args: {
data: {
sessionId: "-1",
page: "0xfff",
frames: [
{
frame: "0xfff",
url: "webpack",
name: ""
}
]
}
}
});
// Chrome DevTools treats this as the primary trace-bootstrap event and
// iterates `args.data.frames`; it must be present or the trace fails to load.
trace.instantEvent({
name: "TracingStartedInBrowser",
id: ++counter,
cat: ["disabled-by-default-devtools.timeline"],
args: {
data: {
sessionId: "-1",
frameTreeNodeId: 1,
persistentIds: true,
frames: [
{
frame: "0xfff",
url: "webpack",
name: ""
}
]
}
}
});
return {
trace,
counter,
profiler,
end: (callback) => {
trace.push("]");
// Wait until the write stream finishes.
fsStream.on("close", () => {
callback();
});
// Tear down the readable trace stream.
trace.push(null);
}
};
};
const PLUGIN_NAME = "ProfilingPlugin";
class ProfilingPlugin {
/**
* Creates an instance of ProfilingPlugin.
* @param {ProfilingPluginOptions=} options options object
*/
constructor(options = {}) {
if (typeof options.outputPath === "string") {
options.outputPath = fileUrlToPath(options.outputPath);
}
/** @type {ProfilingPluginOptions} */
this.options = options;
}
/**
* Applies the plugin by registering its hooks on the compiler.
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
compiler.hooks.validate.tap(PLUGIN_NAME, () => {
compiler.validate(
() => require("../../schemas/plugins/debug/ProfilingPlugin.json"),
this.options,
{
name: "Profiling Plugin",
baseDataPath: "options"
},
(options) =>
require("../../schemas/plugins/debug/ProfilingPlugin.check")(options)
);
});
const tracer = createTrace(
/** @type {IntermediateFileSystem} */
(compiler.intermediateFileSystem),
this.options.outputPath || "events.json"
);
tracer.profiler.startProfiling();
// Compiler Hooks
for (const hookName of Object.keys(compiler.hooks)) {
const hook =
compiler.hooks[/** @type {keyof Compiler["hooks"]} */ (hookName)];
if (hook) {
interceptTaps(hook, makeInterceptorFor("Compiler", tracer)(hookName));
}
}
for (const hookName of Object.keys(compiler.resolverFactory.hooks)) {
const hook =
compiler.resolverFactory.hooks[
/** @type {keyof ResolverFactory["hooks"]} */
(hookName)
];
if (hook) {
interceptTaps(hook, makeInterceptorFor("Resolver", tracer)(hookName));
}
}
compiler.hooks.compilation.tap(
PLUGIN_NAME,
(compilation, { normalModuleFactory, contextModuleFactory }) => {
interceptAllHooksFor(compilation, tracer, "Compilation");
interceptAllHooksFor(
normalModuleFactory,
tracer,
"Normal Module Factory"
);
interceptAllHooksFor(
contextModuleFactory,
tracer,
"Context Module Factory"
);
interceptAllParserHooks(normalModuleFactory, tracer);
interceptAllGeneratorHooks(normalModuleFactory, tracer);
interceptAllJavascriptModulesPluginHooks(compilation, tracer);
interceptAllCssModulesPluginHooks(compilation, tracer);
}
);
// We need to write out the CPU profile when we are all done.
compiler.hooks.done.tapAsync(
{
name: PLUGIN_NAME,
stage: Infinity
},
(stats, callback) => {
if (compiler.watchMode) return callback();
tracer.profiler.stopProfiling().then((parsedResults) => {
if (parsedResults === undefined) {
tracer.profiler.destroy();
tracer.end(callback);
return;
}
const cpuStartTime = parsedResults.profile.startTime;
const cpuEndTime = parsedResults.profile.endTime;
tracer.trace.completeEvent({
name: "TaskQueueManager::ProcessTaskFromWorkQueue",
id: ++tracer.counter,
cat: ["toplevel"],
ts: cpuStartTime,
args: {
// eslint-disable-next-line camelcase
src_file: "../../ipc/ipc_moji_bootstrap.cc",
// eslint-disable-next-line camelcase
src_func: "Accept"
}
});
tracer.trace.completeEvent({
name: "EvaluateScript",
id: ++tracer.counter,
cat: ["devtools.timeline"],
ts: cpuStartTime,
dur: cpuEndTime - cpuStartTime,
args: {
data: {
url: "webpack",
lineNumber: 1,
columnNumber: 1,
frame: "0xFFF"
}
}
});
tracer.trace.instantEvent({
name: "CpuProfile",
id: ++tracer.counter,
cat: ["disabled-by-default-devtools.timeline"],
ts: cpuEndTime,
args: {
data: {
cpuProfile: parsedResults.profile
}
}
});
tracer.profiler.destroy();
tracer.end(callback);
});
}
);
}
}
/**
* The part of a tapable `HookInterceptor` this plugin sets: it rewraps each tap.
* @typedef {{ register?(tap: FullTap): FullTap }} TapInterceptor
*/
/**
* What intercepting a tapable `Hook` needs of it. Methods, so every hook fits
* whatever its arguments, result and additional tap options.
* @typedef {{ intercept(interceptor: TapInterceptor): void, _fakeHook?: undefined }} HookWithInterceptor
*/
/**
* What intercepting a tapable `HookMap` needs of it, whatever hook it holds.
* @typedef {{ for(key: string): HookWithInterceptor, intercept(interceptor: { factory?(key: string, hook: HookWithInterceptor): HookWithInterceptor }): void, _fakeHook?: undefined }} HookMapWithInterceptor
*/
/** @typedef {Record<string, HookWithInterceptor | HookMapWithInterceptor | FakeHookMarker>} Hooks */
/**
* Intercepts the taps of a hook or every cached and future hook in a hook map.
* @param {HookWithInterceptor | HookMapWithInterceptor} hook the hook or hook map
* @param {TapInterceptor} interceptor what intercepts each tap
* @returns {void}
*/
const interceptTaps = (hook, interceptor) => {
if ("for" in hook) {
const { _map } =
/** @type {HookMapWithInterceptor & { _map: Map<string, HookWithInterceptor> }} */
(hook);
for (const cached of _map.values()) {
cached.intercept(interceptor);
}
hook.intercept({
/**
* @param {string} key the key the hook is created for
* @param {HookWithInterceptor} created the hook created
* @returns {HookWithInterceptor} the same hook, intercepted
*/
factory: (key, created) => {
created.intercept(interceptor);
return created;
}
});
} else {
hook.intercept(interceptor);
}
};
/**
* Intercept all hooks for.
* @param {EXPECTED_OBJECT & { hooks?: Hooks }} instance instance
* @param {Trace} tracer tracer
* @param {string} logLabel log label
*/
const interceptAllHooksFor = (instance, tracer, logLabel) => {
if (Reflect.has(instance, "hooks")) {
const hooks = /** @type {Hooks} */ (instance.hooks);
for (const hookName of Object.keys(hooks)) {
const hook = hooks[hookName];
if (hook && !hook._fakeHook) {
interceptTaps(hook, makeInterceptorFor(logLabel, tracer)(hookName));
}
}
}
};
/**
* Intercept all parser hooks.
* @param {NormalModuleFactory} moduleFactory normal module factory
* @param {Trace} tracer tracer
*/
const interceptAllParserHooks = (moduleFactory, tracer) => {
const moduleTypes = [
...JAVASCRIPT_MODULES,
JSON_MODULE_TYPE,
...WEBASSEMBLY_MODULES,
...CSS_MODULES
];
for (const moduleType of moduleTypes) {
moduleFactory.hooks.parser
.for(moduleType)
.tap(PLUGIN_NAME, (parser, _parserOpts) => {
interceptAllHooksFor(parser, tracer, "Parser");
});
}
};
/**
* Intercept all generator hooks.
* @param {NormalModuleFactory} moduleFactory normal module factory
* @param {Trace} tracer tracer
*/
const interceptAllGeneratorHooks = (moduleFactory, tracer) => {
const moduleTypes = [
...JAVASCRIPT_MODULES,
JSON_MODULE_TYPE,
...WEBASSEMBLY_MODULES,
...CSS_MODULES
];
for (const moduleType of moduleTypes) {
moduleFactory.hooks.generator
.for(moduleType)
.tap(PLUGIN_NAME, (parser, _parserOpts) => {
interceptAllHooksFor(parser, tracer, "Generator");
});
}
};
/**
* Intercept all javascript modules plugin hooks.
* @param {Compilation} compilation compilation
* @param {Trace} tracer tracer
*/
const interceptAllJavascriptModulesPluginHooks = (compilation, tracer) => {
interceptAllHooksFor(
{
hooks:
require("../javascript/JavascriptModulesPlugin").getCompilationHooks(
compilation
)
},
tracer,
"JavascriptModulesPlugin"
);
};
/**
* Intercept all css modules plugin hooks.
* @param {Compilation} compilation compilation
* @param {Trace} tracer tracer
*/
const interceptAllCssModulesPluginHooks = (compilation, tracer) => {
interceptAllHooksFor(
{
hooks: require("../css/CssModulesPlugin").getCompilationHooks(compilation)
},
tracer,
"CssModulesPlugin"
);
};
/**
* A `sync` tap's function, or a `promise` tap's when `R` is a promise.
* @template {unknown[]} A the hook's arguments
* @template R what the tap returns
* @typedef {(...args: A) => R} PluginFunction
*/
/**
* The callback an `async` tap reports its error or result through.
* @template R the hook's result
* @typedef {(error?: Error | null, result?: R) => void} TapCallback
*/
/**
* An `async` tap's function, the hook's callback after its arguments.
* @template {unknown[]} A the hook's arguments
* @template R the hook's result
* @typedef {(...args: [...A, TapCallback<R>]) => void} AsyncPluginFunction
*/
/**
* Creates interceptor for.
* @param {string} instance instance
* @param {Trace} tracer tracer
* @returns {<T, R>(hookName: string) => HookInterceptor<T, R>} interceptor
*/
const makeInterceptorFor = (instance, tracer) => (hookName) => ({
/**
* Returns modified full tap.
* @param {FullTap} tapInfo tap info
* @returns {FullTap} modified full tap
*/
register: (tapInfo) => {
const { name, type, fn: internalFn } = tapInfo;
const newFn =
// Don't tap our own hooks to ensure stream can close cleanly
name === PLUGIN_NAME
? internalFn
: makeNewProfiledTapFn(hookName, tracer, {
name,
type,
fn: internalFn
});
return { ...tapInfo, fn: newFn };
}
});
/**
* Creates new profiled tap fn.
* @template {unknown[]} A the hook's arguments
* @template R the hook's result
* @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 {"sync" | "async" | "promise"} options.type Plugin type (sync | async | promise)
* @param {FullTap["fn"]} options.fn Plugin function, its shape set by `options.type`
* @returns {FullTap["fn"]} Chainable hooked function.
*/
const makeNewProfiledTapFn = (hookName, tracer, { name, type, fn }) => {
const defaultCategory = ["blink.user_timing"];
switch (type) {
case "promise":
return /** @type {PluginFunction<A, Promise<R>>} */ (...args) => {
const id = ++tracer.counter;
tracer.trace.begin({
name,
id,
cat: defaultCategory
});
const promise =
/** @type {PluginFunction<A, Promise<R>>} */
(fn)(...args);
return promise.then((r) => {
tracer.trace.end({
name,
id,
cat: defaultCategory
});
return r;
});
};
case "async":
return /** @type {AsyncPluginFunction<A, R>} */ (...args) => {
const id = ++tracer.counter;
tracer.trace.begin({
name,
id,
cat: defaultCategory
});
const callback = /** @type {TapCallback<R>} */ (args.pop());
/** @type {AsyncPluginFunction<A, R>} */ (fn)(
// `pop()` took the callback off, which the tuple type cannot follow.
.../** @type {A} */ (/** @type {unknown[]} */ (args)),
/**
* Handles the cat callback for this hook.
* @param {Parameters<TapCallback<R>>} r result
*/
(...r) => {
tracer.trace.end({
name,
id,
cat: defaultCategory
});
callback(...r);
}
);
};
case "sync":
return /** @type {PluginFunction<A, R>} */ (...args) => {
const id = ++tracer.counter;
// Do not instrument ourself due to the CPU
// profile needing to be the last event in the trace.
if (name === PLUGIN_NAME) {
return /** @type {PluginFunction<A, R>} */ (fn)(...args);
}
tracer.trace.begin({
name,
id,
cat: defaultCategory
});
/** @type {R} */
let r;
try {
r = /** @type {PluginFunction<A, R>} */ (fn)(...args);
} catch (err) {
tracer.trace.end({
name,
id,
cat: defaultCategory
});
throw err;
}
tracer.trace.end({
name,
id,
cat: defaultCategory
});
return r;
};
default:
return fn;
}
};
ProfilingPlugin.Profiler = Profiler;
module.exports = ProfilingPlugin;