| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561 | /*	MIT License http://www.opensource.org/licenses/mit-license.php*/"use strict";const { Tracer } = require("chrome-trace-event");const {	CSS_MODULES,	JAVASCRIPT_MODULES,	JSON_MODULE_TYPE,	WEBASSEMBLY_MODULES} = require("../ModuleTypeConstants");const createSchemaValidation = require("../util/create-schema-validation");const { dirname, mkdirpSync } = require("../util/fs");/** @typedef {import("inspector").Session} Session *//** @typedef {import("tapable").FullTap} FullTap *//** @typedef {import("../../declarations/plugins/debug/ProfilingPlugin").ProfilingPluginOptions} ProfilingPluginOptions *//** @typedef {import("../Compilation")} Compilation *//** @typedef {import("../Compiler")} Compiler *//** @typedef {import("../ContextModuleFactory")} ContextModuleFactory *//** @typedef {import("../ModuleFactory")} ModuleFactory *//** @typedef {import("../NormalModuleFactory")} NormalModuleFactory *//** @typedef {import("../Parser")} Parser *//** @typedef {import("../ResolverFactory")} ResolverFactory *//** @typedef {import("../util/fs").IntermediateFileSystem} IntermediateFileSystem */const validate = createSchemaValidation(	require("../../schemas/plugins/debug/ProfilingPlugin.check"),	() => require("../../schemas/plugins/debug/ProfilingPlugin.json"),	{		name: "Profiling Plugin",		baseDataPath: "options"	});/** @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 {	/**	 * @param {Inspector} inspector inspector	 */	constructor(inspector) {		/** @type {undefined | Session} */		this.session = undefined;		this.inspector = inspector;		this._startTime = 0;	}	hasSession() {		return this.session !== undefined;	}	startProfiling() {		if (this.inspector === undefined) {			return Promise.resolve();		}		try {			this.session = new /** @type {Inspector} */ (inspector).Session();			/** @type {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")		]);	}	/**	 * @param {string} method method name	 * @param {EXPECTED_OBJECT=} params params	 * @returns {Promise<EXPECTED_ANY | void>} Promise for the result	 */	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();	}	stopProfiling() {		return 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) {				// 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 };		});	}}/** * 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 *//** * @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: ""					}				]			}		}	});	trace.instantEvent({		name: "TracingStartedInBrowser",		id: ++counter,		cat: ["disabled-by-default-devtools.timeline"],		args: {			data: {				sessionId: "-1"			}		}	});	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 {	/**	 * @param {ProfilingPluginOptions=} options options object	 */	constructor(options = {}) {		validate(options);		this.outputPath = options.outputPath || "events.json";	}	/**	 * Apply the plugin	 * @param {Compiler} compiler the compiler instance	 * @returns {void}	 */	apply(compiler) {		const tracer = createTrace(			/** @type {IntermediateFileSystem} */			(compiler.intermediateFileSystem),			this.outputPath		);		tracer.profiler.startProfiling();		// Compiler Hooks		for (const hookName of Object.keys(compiler.hooks)) {			const hook =				compiler.hooks[/** @type {keyof Compiler["hooks"]} */ (hookName)];			if (hook) {				hook.intercept(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) {				hook.intercept(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);				});			}		);	}}/** * @param {EXPECTED_ANY & { hooks: TODO }} instance instance * @param {Trace} tracer tracer * @param {string} logLabel log label */const interceptAllHooksFor = (instance, tracer, logLabel) => {	if (Reflect.has(instance, "hooks")) {		for (const hookName of Object.keys(instance.hooks)) {			const hook = instance.hooks[hookName];			if (hook && !hook._fakeHook) {				hook.intercept(makeInterceptorFor(logLabel, tracer)(hookName));			}		}	}};/** * @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");			});	}};/** * @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");			});	}};/** * @param {Compilation} compilation compilation * @param {Trace} tracer tracer */const interceptAllJavascriptModulesPluginHooks = (compilation, tracer) => {	interceptAllHooksFor(		{			hooks:				require("../javascript/JavascriptModulesPlugin").getCompilationHooks(					compilation				)		},		tracer,		"JavascriptModulesPlugin"	);};/** * @param {Compilation} compilation compilation * @param {Trace} tracer tracer */const interceptAllCssModulesPluginHooks = (compilation, tracer) => {	interceptAllHooksFor(		{			hooks: require("../css/CssModulesPlugin").getCompilationHooks(compilation)		},		tracer,		"CssModulesPlugin"	);};/** @typedef {(...args: EXPECTED_ANY[]) => EXPECTED_ANY | Promise<(...args: EXPECTED_ANY[]) => EXPECTED_ANY>} PluginFunction *//** * @param {string} instance instance * @param {Trace} tracer tracer * @returns {(hookName: string) => TODO} interceptor */const makeInterceptorFor = (instance, tracer) => hookName => ({	/**	 * @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: /** @type {PluginFunction} */ (internalFn)					});		return { ...tapInfo, fn: newFn };	}});/** * @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 {PluginFunction} options.fn Plugin function * @returns {PluginFunction} Chainable hooked function. */const makeNewProfiledTapFn = (hookName, tracer, { name, type, fn }) => {	const defaultCategory = ["blink.user_timing"];	switch (type) {		case "promise":			return (...args) => {				const id = ++tracer.counter;				tracer.trace.begin({					name,					id,					cat: defaultCategory				});				const promise =					/** @type {Promise<(...args: EXPECTED_ANY[]) => EXPECTED_ANY>} */					(fn(...args));				return promise.then(r => {					tracer.trace.end({						name,						id,						cat: defaultCategory					});					return r;				});			};		case "async":			return (...args) => {				const id = ++tracer.counter;				tracer.trace.begin({					name,					id,					cat: defaultCategory				});				const callback = args.pop();				fn(					...args,					/**					 * @param {...EXPECTED_ANY[]} r result					 */					(...r) => {						tracer.trace.end({							name,							id,							cat: defaultCategory						});						callback(...r);					}				);			};		case "sync":			return (...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 fn(...args);				}				tracer.trace.begin({					name,					id,					cat: defaultCategory				});				let r;				try {					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;	}};module.exports = ProfilingPlugin;module.exports.Profiler = Profiler;
 |