HotspotsPlugin.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Alexander Akait @alexander-akait
  4. */
  5. "use strict";
  6. const NormalModule = require("../NormalModule");
  7. const HotspotsWarning = require("../errors/HotspotsWarning");
  8. const { LOADER_TIMING } = require("../loaders/LoaderRunner");
  9. const { compareStrings } = require("../util/comparators");
  10. const { contextify } = require("../util/identifier");
  11. /** @typedef {import("tapable").FullTap} FullTap */
  12. /** @typedef {import("../../declarations/WebpackOptions").PerformanceOptions} PerformanceOptions */
  13. /** @typedef {import("../Compiler")} Compiler */
  14. /** @typedef {import("../errors/HotspotsWarning").HookDetails} HookDetails */
  15. /** @typedef {import("../errors/HotspotsWarning").HotspotDetails} HotspotDetails */
  16. const PLUGIN_NAME = "HotspotsPlugin";
  17. // Enough to name the offenders without listing the whole build.
  18. const MAX_REPORTED_HOTSPOTS = 5;
  19. const MAX_REPORTED_HOOKS = 3;
  20. // Below this nothing is waiting on it, and naming it would bury what is.
  21. const MIN_REPORTED_MS = 100;
  22. // Ahead of every other `compilation` tap: the counters are reset there, and
  23. // anything measured before that reset would be thrown away by it.
  24. const INTERCEPT_STAGE = -10000;
  25. const NS_PER_MS = 1e6;
  26. const MS_PER_SECOND = 1000;
  27. /**
  28. * @param {[number, number]} start what `process.hrtime()` returned
  29. * @returns {number} milliseconds since then
  30. */
  31. const msSince = (start) => {
  32. const [seconds, nanoseconds] = process.hrtime(start);
  33. return seconds * MS_PER_SECOND + nanoseconds / NS_PER_MS;
  34. };
  35. /**
  36. * @param {Map<string, { ms: number, runs: number }>} map where the time is kept
  37. * @param {string} key what ran
  38. * @param {number} ms how long its own code took
  39. * @returns {void}
  40. */
  41. const charge = (map, key, ms) => {
  42. const spent = map.get(key);
  43. if (spent === undefined) {
  44. map.set(key, { ms, runs: 1 });
  45. return;
  46. }
  47. spent.ms += ms;
  48. spent.runs++;
  49. };
  50. class HotspotsPlugin {
  51. /**
  52. * Creates an instance of HotspotsPlugin.
  53. * @param {PerformanceOptions} options the plugin options
  54. */
  55. constructor(options) {
  56. /** @type {PerformanceOptions["hints"]} */
  57. this.hints = options.hints;
  58. }
  59. /**
  60. * Applies the plugin by registering its hooks on the compiler.
  61. * @param {Compiler} compiler the compiler instance
  62. * @returns {void}
  63. */
  64. apply(compiler) {
  65. const hints = this.hints;
  66. if (!hints) return;
  67. /** @typedef {{ ms: number, runs: number }} Spent */
  68. // Kept per kind: a loader and a plugin could otherwise share a name and
  69. // have their runs counted together.
  70. /** @type {Map<string, Spent>} */
  71. const pluginTime = new Map();
  72. /** @type {Map<string, Spent>} */
  73. const loaderTime = new Map();
  74. /** @type {Map<string, number>} */
  75. const hookTime = new Map();
  76. // One frame per thing currently running, innermost last. Only the innermost
  77. // is charged, so whatever calls out to something else is not billed for it.
  78. /** @type {{ child: number }[]} */
  79. const running = [];
  80. /**
  81. * Runs one loader or tap, charging it its own work alone.
  82. * @template T
  83. * @param {Map<string, { ms: number, runs: number }>} map where its time is kept
  84. * @param {string} name what is running
  85. * @param {string | undefined} hookName the hook it runs under, if any
  86. * @param {() => T} run the work, already bound to its arguments
  87. * @returns {T} whatever it returned
  88. */
  89. const measure = (map, name, hookName, run) => {
  90. const frame = { child: 0 };
  91. running.push(frame);
  92. const start = process.hrtime();
  93. try {
  94. return run();
  95. } finally {
  96. const elapsed = msSince(start);
  97. const own = elapsed - frame.child;
  98. running.pop();
  99. charge(map, name, own);
  100. if (hookName !== undefined) {
  101. hookTime.set(hookName, (hookTime.get(hookName) || 0) + own);
  102. }
  103. if (running.length > 0) {
  104. running[running.length - 1].child += elapsed;
  105. }
  106. }
  107. };
  108. /**
  109. * @param {string} hookName the hook being tapped
  110. * @returns {(tapInfo: FullTap) => FullTap} an interceptor for it
  111. */
  112. const registerFor = (hookName) => (tapInfo) => {
  113. const { name, type, fn } = tapInfo;
  114. if (name === PLUGIN_NAME) return tapInfo;
  115. if (type === "async") {
  116. return {
  117. ...tapInfo,
  118. /**
  119. * @param {EXPECTED_ANY[]} args the tap's arguments, callback last
  120. * @returns {EXPECTED_ANY} whatever the tap returned
  121. */
  122. fn: (...args) => {
  123. const callback = args.pop();
  124. /**
  125. * @param {EXPECTED_ANY[]} result what the tap reported
  126. * @returns {EXPECTED_ANY} whatever the callback returned
  127. */
  128. const finish = (...result) =>
  129. measure(pluginTime, name, hookName, () => callback(...result));
  130. // The completion callback is the plugin's own code as well, so it
  131. // is charged to the plugin rather than to whoever resumed it.
  132. return measure(pluginTime, name, hookName, () =>
  133. /** @type {EXPECTED_FUNCTION} */ (fn)(...args, finish)
  134. );
  135. }
  136. };
  137. }
  138. return {
  139. ...tapInfo,
  140. /**
  141. * @param {EXPECTED_ANY[]} args the tap's arguments
  142. * @returns {EXPECTED_ANY} whatever the tap returned
  143. */
  144. fn: (...args) =>
  145. measure(pluginTime, name, hookName, () =>
  146. /** @type {EXPECTED_FUNCTION} */ (fn)(...args)
  147. )
  148. };
  149. };
  150. /**
  151. * @param {EXPECTED_ANY} instance anything carrying tapable hooks
  152. * @returns {void}
  153. */
  154. const interceptAll = (instance) => {
  155. for (const hookName of Object.keys(instance.hooks)) {
  156. const descriptor = Object.getOwnPropertyDescriptor(
  157. instance.hooks,
  158. hookName
  159. );
  160. // A hook kept only as an alias for one that moved is an accessor, and
  161. // reading it is what prints the deprecation — so it is never read.
  162. if (!descriptor || descriptor.get) continue;
  163. const hook = descriptor.value;
  164. // A deprecated hook kept as a stand-in throws when intercepted.
  165. if (hook && !hook._fakeHook && typeof hook.intercept === "function") {
  166. hook.intercept({ register: registerFor(hookName) });
  167. }
  168. }
  169. };
  170. interceptAll(compiler);
  171. compiler.hooks.compilation.tap(
  172. { name: PLUGIN_NAME, stage: INTERCEPT_STAGE },
  173. (compilation, { normalModuleFactory, contextModuleFactory }) => {
  174. // Each build reports its own time. Watching keeps the compiler, so
  175. // without this every rebuild would add to the one before it.
  176. pluginTime.clear();
  177. loaderTime.clear();
  178. hookTime.clear();
  179. running.length = 0;
  180. interceptAll(compilation);
  181. interceptAll(normalModuleFactory);
  182. interceptAll(contextModuleFactory);
  183. const context = compiler.context;
  184. // Loaders are not taps, so they are measured where they run instead.
  185. NormalModule.getCompilationHooks(compilation).loader.tap(
  186. PLUGIN_NAME,
  187. (loaderContext) => {
  188. /** @type {EXPECTED_ANY} */
  189. (loaderContext)[LOADER_TIMING] =
  190. /**
  191. * @param {{ path: string }} loader the loader about to run
  192. * @param {() => EXPECTED_ANY} run its own execution
  193. * @returns {EXPECTED_ANY} whatever it returned
  194. */
  195. (loader, run) =>
  196. measure(
  197. loaderTime,
  198. contextify(context, loader.path, compiler.root),
  199. undefined,
  200. run
  201. );
  202. }
  203. );
  204. }
  205. );
  206. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  207. // `afterSeal` is past the hash, which folds every message into it — a
  208. // hint reported earlier would change the build's identity.
  209. compilation.hooks.afterSeal.tap(PLUGIN_NAME, () => {
  210. /** @type {HotspotDetails[]} */
  211. const hotspots = [];
  212. // Loaders and plugins never name the same thing, so one ranking over
  213. // both double counts nothing.
  214. for (const [kind, map] of /** @type {const} */ ([
  215. ["loader", loaderTime],
  216. ["plugin", pluginTime]
  217. ])) {
  218. for (const [name, spent] of map) {
  219. if (spent.ms < MIN_REPORTED_MS) continue;
  220. hotspots.push({ kind, name, ms: spent.ms, runs: spent.runs });
  221. }
  222. }
  223. if (hotspots.length === 0) return;
  224. // Ties break by name, though two rarely take the same time.
  225. hotspots.sort((a, b) => b.ms - a.ms || compareStrings(a.name, b.name));
  226. /** @type {HookDetails[]} */
  227. const hooks = [...hookTime]
  228. .filter(([, ms]) => ms >= MIN_REPORTED_MS)
  229. .map(([name, ms]) => ({ name, ms }))
  230. .sort((a, b) => b.ms - a.ms || compareStrings(a.name, b.name))
  231. .slice(0, MAX_REPORTED_HOOKS);
  232. const warning = new HotspotsWarning(
  233. hotspots.slice(0, MAX_REPORTED_HOTSPOTS),
  234. hotspots.length,
  235. hooks
  236. );
  237. if (hints === "error") {
  238. compilation.errors.push(warning);
  239. } else if (hints === "stats") {
  240. compilation.hints.push(warning);
  241. } else {
  242. compilation.warnings.push(warning);
  243. }
  244. });
  245. });
  246. }
  247. }
  248. module.exports = HotspotsPlugin;