| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- /*
- MIT License http://www.opensource.org/licenses/mit-license.php
- Author Alexander Akait @alexander-akait
- */
- "use strict";
- const NormalModule = require("../NormalModule");
- const HotspotsWarning = require("../errors/HotspotsWarning");
- const { LOADER_TIMING } = require("../loaders/LoaderRunner");
- const { compareStrings } = require("../util/comparators");
- const { contextify } = require("../util/identifier");
- /** @typedef {import("tapable").FullTap} FullTap */
- /** @typedef {import("../../declarations/WebpackOptions").PerformanceOptions} PerformanceOptions */
- /** @typedef {import("../Compiler")} Compiler */
- /** @typedef {import("../errors/HotspotsWarning").HookDetails} HookDetails */
- /** @typedef {import("../errors/HotspotsWarning").HotspotDetails} HotspotDetails */
- const PLUGIN_NAME = "HotspotsPlugin";
- // Enough to name the offenders without listing the whole build.
- const MAX_REPORTED_HOTSPOTS = 5;
- const MAX_REPORTED_HOOKS = 3;
- // Below this nothing is waiting on it, and naming it would bury what is.
- const MIN_REPORTED_MS = 100;
- // Ahead of every other `compilation` tap: the counters are reset there, and
- // anything measured before that reset would be thrown away by it.
- const INTERCEPT_STAGE = -10000;
- const NS_PER_MS = 1e6;
- const MS_PER_SECOND = 1000;
- /**
- * @param {[number, number]} start what `process.hrtime()` returned
- * @returns {number} milliseconds since then
- */
- const msSince = (start) => {
- const [seconds, nanoseconds] = process.hrtime(start);
- return seconds * MS_PER_SECOND + nanoseconds / NS_PER_MS;
- };
- /**
- * @param {Map<string, { ms: number, runs: number }>} map where the time is kept
- * @param {string} key what ran
- * @param {number} ms how long its own code took
- * @returns {void}
- */
- const charge = (map, key, ms) => {
- const spent = map.get(key);
- if (spent === undefined) {
- map.set(key, { ms, runs: 1 });
- return;
- }
- spent.ms += ms;
- spent.runs++;
- };
- class HotspotsPlugin {
- /**
- * Creates an instance of HotspotsPlugin.
- * @param {PerformanceOptions} options the plugin options
- */
- constructor(options) {
- /** @type {PerformanceOptions["hints"]} */
- this.hints = options.hints;
- }
- /**
- * Applies the plugin by registering its hooks on the compiler.
- * @param {Compiler} compiler the compiler instance
- * @returns {void}
- */
- apply(compiler) {
- const hints = this.hints;
- if (!hints) return;
- /** @typedef {{ ms: number, runs: number }} Spent */
- // Kept per kind: a loader and a plugin could otherwise share a name and
- // have their runs counted together.
- /** @type {Map<string, Spent>} */
- const pluginTime = new Map();
- /** @type {Map<string, Spent>} */
- const loaderTime = new Map();
- /** @type {Map<string, number>} */
- const hookTime = new Map();
- // One frame per thing currently running, innermost last. Only the innermost
- // is charged, so whatever calls out to something else is not billed for it.
- /** @type {{ child: number }[]} */
- const running = [];
- /**
- * Runs one loader or tap, charging it its own work alone.
- * @template T
- * @param {Map<string, { ms: number, runs: number }>} map where its time is kept
- * @param {string} name what is running
- * @param {string | undefined} hookName the hook it runs under, if any
- * @param {() => T} run the work, already bound to its arguments
- * @returns {T} whatever it returned
- */
- const measure = (map, name, hookName, run) => {
- const frame = { child: 0 };
- running.push(frame);
- const start = process.hrtime();
- try {
- return run();
- } finally {
- const elapsed = msSince(start);
- const own = elapsed - frame.child;
- running.pop();
- charge(map, name, own);
- if (hookName !== undefined) {
- hookTime.set(hookName, (hookTime.get(hookName) || 0) + own);
- }
- if (running.length > 0) {
- running[running.length - 1].child += elapsed;
- }
- }
- };
- /**
- * @param {string} hookName the hook being tapped
- * @returns {(tapInfo: FullTap) => FullTap} an interceptor for it
- */
- const registerFor = (hookName) => (tapInfo) => {
- const { name, type, fn } = tapInfo;
- if (name === PLUGIN_NAME) return tapInfo;
- if (type === "async") {
- return {
- ...tapInfo,
- /**
- * @param {EXPECTED_ANY[]} args the tap's arguments, callback last
- * @returns {EXPECTED_ANY} whatever the tap returned
- */
- fn: (...args) => {
- const callback = args.pop();
- /**
- * @param {EXPECTED_ANY[]} result what the tap reported
- * @returns {EXPECTED_ANY} whatever the callback returned
- */
- const finish = (...result) =>
- measure(pluginTime, name, hookName, () => callback(...result));
- // The completion callback is the plugin's own code as well, so it
- // is charged to the plugin rather than to whoever resumed it.
- return measure(pluginTime, name, hookName, () =>
- /** @type {EXPECTED_FUNCTION} */ (fn)(...args, finish)
- );
- }
- };
- }
- return {
- ...tapInfo,
- /**
- * @param {EXPECTED_ANY[]} args the tap's arguments
- * @returns {EXPECTED_ANY} whatever the tap returned
- */
- fn: (...args) =>
- measure(pluginTime, name, hookName, () =>
- /** @type {EXPECTED_FUNCTION} */ (fn)(...args)
- )
- };
- };
- /**
- * @param {EXPECTED_ANY} instance anything carrying tapable hooks
- * @returns {void}
- */
- const interceptAll = (instance) => {
- for (const hookName of Object.keys(instance.hooks)) {
- const descriptor = Object.getOwnPropertyDescriptor(
- instance.hooks,
- hookName
- );
- // A hook kept only as an alias for one that moved is an accessor, and
- // reading it is what prints the deprecation — so it is never read.
- if (!descriptor || descriptor.get) continue;
- const hook = descriptor.value;
- // A deprecated hook kept as a stand-in throws when intercepted.
- if (hook && !hook._fakeHook && typeof hook.intercept === "function") {
- hook.intercept({ register: registerFor(hookName) });
- }
- }
- };
- interceptAll(compiler);
- compiler.hooks.compilation.tap(
- { name: PLUGIN_NAME, stage: INTERCEPT_STAGE },
- (compilation, { normalModuleFactory, contextModuleFactory }) => {
- // Each build reports its own time. Watching keeps the compiler, so
- // without this every rebuild would add to the one before it.
- pluginTime.clear();
- loaderTime.clear();
- hookTime.clear();
- running.length = 0;
- interceptAll(compilation);
- interceptAll(normalModuleFactory);
- interceptAll(contextModuleFactory);
- const context = compiler.context;
- // Loaders are not taps, so they are measured where they run instead.
- NormalModule.getCompilationHooks(compilation).loader.tap(
- PLUGIN_NAME,
- (loaderContext) => {
- /** @type {EXPECTED_ANY} */
- (loaderContext)[LOADER_TIMING] =
- /**
- * @param {{ path: string }} loader the loader about to run
- * @param {() => EXPECTED_ANY} run its own execution
- * @returns {EXPECTED_ANY} whatever it returned
- */
- (loader, run) =>
- measure(
- loaderTime,
- contextify(context, loader.path, compiler.root),
- undefined,
- run
- );
- }
- );
- }
- );
- compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
- // `afterSeal` is past the hash, which folds every message into it — a
- // hint reported earlier would change the build's identity.
- compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
- /** @type {HotspotDetails[]} */
- const hotspots = [];
- // Loaders and plugins never name the same thing, so one ranking over
- // both double counts nothing.
- for (const [kind, map] of /** @type {const} */ ([
- ["loader", loaderTime],
- ["plugin", pluginTime]
- ])) {
- for (const [name, spent] of map) {
- if (spent.ms < MIN_REPORTED_MS) continue;
- hotspots.push({ kind, name, ms: spent.ms, runs: spent.runs });
- }
- }
- if (hotspots.length === 0) return;
- // Ties break by name, though two rarely take the same time.
- hotspots.sort((a, b) => b.ms - a.ms || compareStrings(a.name, b.name));
- /** @type {HookDetails[]} */
- const hooks = [...hookTime]
- .filter(([, ms]) => ms >= MIN_REPORTED_MS)
- .map(([name, ms]) => ({ name, ms }))
- .sort((a, b) => b.ms - a.ms || compareStrings(a.name, b.name))
- .slice(0, MAX_REPORTED_HOOKS);
- const warning = new HotspotsWarning(
- hotspots.slice(0, MAX_REPORTED_HOTSPOTS),
- hotspots.length,
- hooks
- );
- if (hints === "error") {
- compilation.errors.push(warning);
- } else if (hints === "stats") {
- compilation.hints.push(warning);
- } else {
- compilation.warnings.push(warning);
- }
- });
- });
- }
- }
- module.exports = HotspotsPlugin;
|