webpack/lib/debug/ProfilingPlugin.js

497 lines
12 KiB
JavaScript
Raw Normal View History

2018-07-30 23:08:51 +08:00
/*
MIT License http://www.opensource.org/licenses/mit-license.php
*/
"use strict";
const { Tracer } = require("chrome-trace-event");
const {
JAVASCRIPT_MODULE_TYPE_AUTO,
JAVASCRIPT_MODULE_TYPE_DYNAMIC,
JAVASCRIPT_MODULE_TYPE_ESM,
WEBASSEMBLY_MODULE_TYPE_ASYNC,
WEBASSEMBLY_MODULE_TYPE_SYNC,
JSON_MODULE_TYPE
} = require("../ModuleTypeConstants");
const createSchemaValidation = require("../util/create-schema-validation");
const { dirname, mkdirpSync } = require("../util/fs");
/** @typedef {import("../../declarations/plugins/debug/ProfilingPlugin").ProfilingPluginOptions} ProfilingPluginOptions */
2024-03-14 02:23:33 +08:00
/** @typedef {import("../Compilation")} Compilation */
2023-05-22 02:58:37 +08:00
/** @typedef {import("../Compiler")} Compiler */
2024-03-14 02:23:33 +08:00
/** @typedef {import("../ContextModuleFactory")} ContextModuleFactory */
/** @typedef {import("../ModuleFactory")} ModuleFactory */
/** @typedef {import("../NormalModuleFactory")} NormalModuleFactory */
/** @typedef {import("../util/fs").IntermediateFileSystem} IntermediateFileSystem */
2024-03-14 02:23:33 +08:00
/** @typedef {TODO} Inspector */
const validate = createSchemaValidation(
require("../../schemas/plugins/debug/ProfilingPlugin.check.js"),
() => require("../../schemas/plugins/debug/ProfilingPlugin.json"),
{
name: "Profiling Plugin",
baseDataPath: "options"
}
);
2024-03-14 02:23:33 +08:00
/** @type {Inspector | undefined} */
2017-12-26 11:12:08 +08:00
let inspector = undefined;
try {
2024-01-13 21:28:53 +08:00
// eslint-disable-next-line n/no-unsupported-features/node-builtins
inspector = require("inspector");
2018-02-25 09:00:20 +08:00
} catch (e) {
2017-12-26 11:12:08 +08:00
console.log("Unable to CPU profile in < node 8.0");
}
class Profiler {
2024-03-14 02:23:33 +08:00
/**
* @param {Inspector} inspector inspector
*/
2017-12-26 11:12:08 +08:00
constructor(inspector) {
this.session = undefined;
this.inspector = inspector;
this._startTime = 0;
2017-12-26 11:12:08 +08:00
}
hasSession() {
return this.session !== undefined;
}
startProfiling() {
2018-02-25 09:00:20 +08:00
if (this.inspector === undefined) {
2017-12-26 11:12:08 +08:00
return Promise.resolve();
}
try {
this.session = new inspector.Session();
this.session.connect();
2018-02-25 09:00:20 +08:00
} catch (_) {
2017-12-26 11:12:08 +08:00
this.session = undefined;
return Promise.resolve();
}
const hrtime = process.hrtime();
this._startTime = hrtime[0] * 1000000 + Math.round(hrtime[1] / 1000);
2017-12-26 11:12:08 +08:00
return Promise.all([
this.sendCommand("Profiler.setSamplingInterval", {
interval: 100
}),
this.sendCommand("Profiler.enable"),
2018-02-25 09:00:20 +08:00
this.sendCommand("Profiler.start")
2017-12-26 11:12:08 +08:00
]);
}
2023-05-22 02:58:37 +08:00
/**
* @param {string} method method name
* @param {object} [params] params
* @returns {Promise<TODO>} Promise for the result
*/
2017-12-26 11:12:08 +08:00
sendCommand(method, params) {
2018-02-25 09:00:20 +08:00
if (this.hasSession()) {
2017-12-26 11:12:08 +08:00
return new Promise((res, rej) => {
return this.session.post(method, params, (err, params) => {
2018-02-25 09:00:20 +08:00
if (err !== null) {
2017-12-26 11:12:08 +08:00
rej(err);
} else {
res(params);
}
});
});
}
2024-07-31 04:21:27 +08:00
return Promise.resolve();
2017-12-26 11:12:08 +08:00
}
destroy() {
2018-02-25 09:00:20 +08:00
if (this.hasSession()) {
2017-12-26 11:12:08 +08:00
this.session.disconnect();
}
return Promise.resolve();
}
stopProfiling() {
return this.sendCommand("Profiler.stop").then(({ profile }) => {
const hrtime = process.hrtime();
const endTime = hrtime[0] * 1000000 + Math.round(hrtime[1] / 1000);
2023-05-01 06:11:26 +08:00
// Avoid coverage problems due indirect changes
/* istanbul ignore next */
if (profile.startTime < this._startTime || profile.endTime > endTime) {
// In some cases timestamps mismatch and we need to adjust them
// Both process.hrtime and the inspector timestamps claim to be relative
// to a unknown point in time. But they do not guarantee that this is the
// same point in time.
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 };
});
2017-12-26 11:12:08 +08:00
}
}
/**
* an object that wraps Tracer and Profiler with a counter
2024-06-11 21:09:50 +08:00
* @typedef {object} Trace
* @property {Tracer} trace instance of Tracer
* @property {number} counter Counter
* @property {Profiler} profiler instance of Profiler
* @property {Function} end the end function
*/
2017-12-26 11:12:08 +08:00
/**
* @param {IntermediateFileSystem} fs filesystem used for output
* @param {string} outputPath The location where to write the log.
* @returns {Trace} The trace object
2017-12-26 11:12:08 +08:00
*/
const createTrace = (fs, outputPath) => {
const trace = new Tracer();
2024-03-14 02:23:33 +08:00
const profiler = new Profiler(/** @type {Inspector} */ (inspector));
2018-12-28 23:21:17 +08:00
if (/\/|\\/.test(outputPath)) {
const dirPath = dirname(fs, outputPath);
mkdirpSync(fs, dirPath);
}
const fsStream = fs.createWriteStream(outputPath);
2017-12-26 11:12:08 +08:00
let counter = 0;
trace.pipe(fsStream);
2017-12-26 11:12:08 +08:00
// 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",
2018-02-25 09:00:20 +08:00
frames: [
{
frame: "0xfff",
url: "webpack",
name: ""
}
]
2017-12-26 11:12:08 +08:00
}
}
});
trace.instantEvent({
name: "TracingStartedInBrowser",
id: ++counter,
cat: ["disabled-by-default-devtools.timeline"],
args: {
data: {
sessionId: "-1"
2018-02-25 09:00:20 +08:00
}
2017-12-26 11:12:08 +08:00
}
});
return {
trace,
counter,
profiler,
end: callback => {
trace.push("]");
// Wait until the write stream finishes.
2020-06-02 21:51:45 +08:00
fsStream.on("close", () => {
callback();
});
// Tear down the readable trace stream.
2018-07-05 16:32:53 +08:00
trace.push(null);
}
2017-12-26 11:12:08 +08:00
};
};
2017-12-26 11:12:08 +08:00
const PLUGIN_NAME = "ProfilingPlugin";
2017-12-26 11:12:08 +08:00
class ProfilingPlugin {
/**
2019-08-07 21:55:03 +08:00
* @param {ProfilingPluginOptions=} options options object
*/
2019-08-07 21:55:03 +08:00
constructor(options = {}) {
validate(options);
2019-08-07 21:55:03 +08:00
this.outputPath = options.outputPath || "events.json";
2017-12-26 11:12:08 +08:00
}
2023-05-22 02:58:37 +08:00
/**
* Apply the plugin
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
2017-12-26 11:12:08 +08:00
apply(compiler) {
const tracer = createTrace(
2024-03-14 02:23:33 +08:00
/** @type {IntermediateFileSystem} */
(compiler.intermediateFileSystem),
this.outputPath
);
2017-12-26 11:12:08 +08:00
tracer.profiler.startProfiling();
// Compiler Hooks
Object.keys(compiler.hooks).forEach(hookName => {
const hook = compiler.hooks[hookName];
if (hook) {
hook.intercept(makeInterceptorFor("Compiler", tracer)(hookName));
}
2017-12-26 11:12:08 +08:00
});
Object.keys(compiler.resolverFactory.hooks).forEach(hookName => {
const hook = compiler.resolverFactory.hooks[hookName];
if (hook) {
hook.intercept(makeInterceptorFor("Resolver", tracer)(hookName));
}
2017-12-26 11:12:08 +08:00
});
2018-02-25 09:00:20 +08:00
compiler.hooks.compilation.tap(
PLUGIN_NAME,
2018-02-25 09:00:20 +08:00
(compilation, { normalModuleFactory, contextModuleFactory }) => {
interceptAllHooksFor(compilation, tracer, "Compilation");
interceptAllHooksFor(
normalModuleFactory,
tracer,
"Normal Module Factory"
);
interceptAllHooksFor(
contextModuleFactory,
tracer,
"Context Module Factory"
);
interceptAllParserHooks(normalModuleFactory, tracer);
interceptAllJavascriptModulesPluginHooks(compilation, tracer);
2018-02-25 09:00:20 +08:00
}
);
2017-12-26 11:12:08 +08:00
// We need to write out the CPU profile when we are all done.
compiler.hooks.done.tapAsync(
2018-02-25 09:00:20 +08:00
{
name: PLUGIN_NAME,
2018-02-25 09:00:20 +08:00
stage: Infinity
},
(stats, callback) => {
if (compiler.watchMode) return callback();
2018-02-25 09:00:20 +08:00
tracer.profiler.stopProfiling().then(parsedResults => {
if (parsedResults === undefined) {
tracer.profiler.destroy();
tracer.end(callback);
2018-02-25 09:00:20 +08:00
return;
2017-12-26 11:12:08 +08:00
}
2018-02-25 09:00:20 +08:00
const cpuStartTime = parsedResults.profile.startTime;
const cpuEndTime = parsedResults.profile.endTime;
tracer.trace.completeEvent({
name: "TaskQueueManager::ProcessTaskFromWorkQueue",
id: ++tracer.counter,
cat: ["toplevel"],
ts: cpuStartTime,
args: {
src_file: "../../ipc/ipc_moji_bootstrap.cc",
src_func: "Accept"
2017-12-26 11:12:08 +08:00
}
2018-02-25 09:00:20 +08:00
});
2017-12-26 11:12:08 +08:00
2018-02-25 09:00:20 +08:00
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"
}
2017-12-26 11:12:08 +08:00
}
2018-02-25 09:00:20 +08:00
});
2017-12-26 11:12:08 +08:00
2018-02-25 09:00:20 +08:00
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);
2018-02-25 09:00:20 +08:00
});
}
);
2017-12-26 11:12:08 +08:00
}
}
2024-03-14 02:23:33 +08:00
/**
* @param {any} instance instance
* @param {Trace} tracer tracer
* @param {string} logLabel log label
*/
2017-12-26 11:12:08 +08:00
const interceptAllHooksFor = (instance, tracer, logLabel) => {
2018-02-25 09:00:20 +08:00
if (Reflect.has(instance, "hooks")) {
Object.keys(instance.hooks).forEach(hookName => {
const hook = instance.hooks[hookName];
if (hook && !hook._fakeHook) {
hook.intercept(makeInterceptorFor(logLabel, tracer)(hookName));
}
});
}
2017-12-26 11:12:08 +08:00
};
2024-03-14 02:23:33 +08:00
/**
* @param {NormalModuleFactory} moduleFactory normal module factory
* @param {Trace} tracer tracer
*/
2017-12-26 11:12:08 +08:00
const interceptAllParserHooks = (moduleFactory, tracer) => {
const moduleTypes = [
JAVASCRIPT_MODULE_TYPE_AUTO,
JAVASCRIPT_MODULE_TYPE_DYNAMIC,
JAVASCRIPT_MODULE_TYPE_ESM,
JSON_MODULE_TYPE,
WEBASSEMBLY_MODULE_TYPE_ASYNC,
WEBASSEMBLY_MODULE_TYPE_SYNC
2017-12-26 11:12:08 +08:00
];
moduleTypes.forEach(moduleType => {
2018-02-25 09:00:20 +08:00
moduleFactory.hooks.parser
.for(moduleType)
.tap(PLUGIN_NAME, (parser, parserOpts) => {
2018-02-25 09:00:20 +08:00
interceptAllHooksFor(parser, tracer, "Parser");
});
2017-12-26 11:12:08 +08:00
});
};
2024-03-14 02:23:33 +08:00
/**
* @param {Compilation} compilation compilation
* @param {Trace} tracer tracer
*/
const interceptAllJavascriptModulesPluginHooks = (compilation, tracer) => {
interceptAllHooksFor(
{
2021-05-11 15:31:46 +08:00
hooks:
require("../javascript/JavascriptModulesPlugin").getCompilationHooks(
compilation
)
},
tracer,
"JavascriptModulesPlugin"
);
};
2018-02-25 09:00:20 +08:00
const makeInterceptorFor = (instance, tracer) => hookName => ({
register: tapInfo => {
const { name, type, fn } = tapInfo;
2022-01-18 12:45:42 +08:00
const newFn =
// Don't tap our own hooks to ensure stream can close cleanly
name === PLUGIN_NAME
2022-01-18 12:45:42 +08:00
? fn
: makeNewProfiledTapFn(hookName, tracer, {
name,
type,
fn
2024-07-31 05:43:19 +08:00
});
2024-07-31 04:54:55 +08:00
return { ...tapInfo, fn: newFn };
2017-12-26 11:12:08 +08:00
}
});
// TODO improve typing
/** @typedef {(...args: TODO[]) => void | Promise<TODO>} PluginFunction */
2017-12-26 11:12:08 +08:00
/**
* @param {string} hookName Name of the hook to profile.
* @param {Trace} tracer The trace object.
2018-04-12 00:53:38 +08:00
* @param {object} options Options for the profiled fn.
2018-04-12 17:05:34 +08:00
* @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.
2017-12-26 11:12:08 +08:00
*/
2018-04-12 00:53:38 +08:00
const makeNewProfiledTapFn = (hookName, tracer, { name, type, fn }) => {
2017-12-26 11:12:08 +08:00
const defaultCategory = ["blink.user_timing"];
2018-02-25 09:00:20 +08:00
switch (type) {
2017-12-26 11:12:08 +08:00
case "promise":
2018-02-25 09:00:20 +08:00
return (...args) => {
2017-12-26 11:12:08 +08:00
const id = ++tracer.counter;
tracer.trace.begin({
name,
id,
cat: defaultCategory
});
2018-04-12 17:05:34 +08:00
const promise = /** @type {Promise<*>} */ (fn(...args));
return promise.then(r => {
2017-12-26 11:12:08 +08:00
tracer.trace.end({
name,
id,
cat: defaultCategory
});
return r;
});
};
case "async":
2018-02-25 09:00:20 +08:00
return (...args) => {
2017-12-26 11:12:08 +08:00
const id = ++tracer.counter;
tracer.trace.begin({
name,
id,
cat: defaultCategory
});
const callback = args.pop();
2017-12-26 11:12:08 +08:00
fn(...args, (...r) => {
tracer.trace.end({
name,
id,
cat: defaultCategory
});
callback(...r);
});
};
case "sync":
2018-02-25 09:00:20 +08:00
return (...args) => {
2017-12-26 11:12:08 +08:00
const id = ++tracer.counter;
2018-02-26 10:38:03 +08:00
// Do not instrument ourself due to the CPU
// profile needing to be the last event in the trace.
if (name === PLUGIN_NAME) {
return fn(...args);
}
2017-12-26 11:12:08 +08:00
tracer.trace.begin({
name,
id,
cat: defaultCategory
});
let r;
try {
r = fn(...args);
2018-02-25 09:00:20 +08:00
} catch (error) {
2017-12-26 11:12:08 +08:00
tracer.trace.end({
name,
id,
cat: defaultCategory
});
throw error;
}
tracer.trace.end({
name,
id,
cat: defaultCategory
});
return r;
};
default:
break;
}
};
module.exports = ProfilingPlugin;
module.exports.Profiler = Profiler;