LimitChunkCountPlugin.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { STAGE_ADVANCED } = require("../OptimizationStages");
  7. const LazyBucketSortedSet = require("../util/LazyBucketSortedSet");
  8. const { compareChunks } = require("../util/comparators");
  9. /**
  10. * @import {
  11. * LimitChunkCountPluginOptions
  12. * } from "../../declarations/plugins/optimize/LimitChunkCountPlugin"
  13. */
  14. /** @import Chunk from "../Chunk" */
  15. /** @import Compiler from "../Compiler" */
  16. /**
  17. * Defines the chunk combination type used by this module.
  18. * @typedef {object} ChunkCombination
  19. * @property {boolean} deleted this is set to true when combination was removed
  20. * @property {number} sizeDiff
  21. * @property {number} integratedSize
  22. * @property {Chunk} a
  23. * @property {Chunk} b
  24. * @property {number} aIdx
  25. * @property {number} bIdx
  26. * @property {number} aSize
  27. * @property {number} bSize
  28. */
  29. /**
  30. * Adds the provided map to this object.
  31. * @template K, V
  32. * @param {Map<K, Set<V>>} map map
  33. * @param {K} key key
  34. * @param {V} value value
  35. */
  36. const addToSetMap = (map, key, value) => {
  37. const set = map.get(key);
  38. if (set === undefined) {
  39. map.set(key, new Set([value]));
  40. } else {
  41. set.add(value);
  42. }
  43. };
  44. const PLUGIN_NAME = "LimitChunkCountPlugin";
  45. class LimitChunkCountPlugin {
  46. /**
  47. * Creates an instance of LimitChunkCountPlugin.
  48. * @param {LimitChunkCountPluginOptions=} options options object
  49. */
  50. constructor(options = { maxChunks: 1 }) {
  51. /** @type {LimitChunkCountPluginOptions} */
  52. this.options = options;
  53. }
  54. /**
  55. * Applies the plugin by registering its hooks on the compiler.
  56. * @param {Compiler} compiler the webpack compiler
  57. * @returns {void}
  58. */
  59. apply(compiler) {
  60. compiler.hooks.validate.tap(PLUGIN_NAME, () => {
  61. compiler.validate(
  62. () =>
  63. require("../../schemas/plugins/optimize/LimitChunkCountPlugin.json"),
  64. this.options,
  65. {
  66. name: "Limit Chunk Count Plugin",
  67. baseDataPath: "options"
  68. },
  69. (options) =>
  70. require("../../schemas/plugins/optimize/LimitChunkCountPlugin.check")(
  71. options
  72. )
  73. );
  74. });
  75. compiler.hooks.compilation.tap(PLUGIN_NAME, (compilation) => {
  76. compilation.hooks.optimizeChunks.tap(
  77. {
  78. name: PLUGIN_NAME,
  79. stage: STAGE_ADVANCED
  80. },
  81. (chunks) => {
  82. const chunkGraph = compilation.chunkGraph;
  83. const maxChunks = this.options.maxChunks;
  84. if (!maxChunks) return;
  85. if (maxChunks < 1) return;
  86. if (compilation.chunks.size <= maxChunks) return;
  87. let remainingChunksToMerge = compilation.chunks.size - maxChunks;
  88. // order chunks in a deterministic way
  89. const compareChunksWithGraph = compareChunks(chunkGraph);
  90. /** @type {Chunk[]} */
  91. const orderedChunks = [...chunks].sort(compareChunksWithGraph);
  92. // create a lazy sorted data structure to keep all combinations
  93. // this is large. Size = chunks * (chunks - 1) / 2
  94. // It uses a multi layer bucket sort plus normal sort in the last layer
  95. // It's also lazy so only accessed buckets are sorted
  96. /** @type {LazyBucketSortedSet<ChunkCombination, number>} */
  97. const combinations = new LazyBucketSortedSet(
  98. // Layer 1: ordered by largest size benefit
  99. (c) => c.sizeDiff,
  100. (a, b) => b - a,
  101. // Layer 2: ordered by smallest combined size
  102. /**
  103. * Handles the stage callback for this hook.
  104. * @param {ChunkCombination} c combination
  105. * @returns {number} integrated size
  106. */
  107. (c) => c.integratedSize,
  108. /**
  109. * Handles the callback logic for this hook.
  110. * @param {number} a a
  111. * @param {number} b b
  112. * @returns {number} result
  113. */
  114. (a, b) => a - b,
  115. // Layer 3: ordered by position difference in orderedChunk (-> to be deterministic)
  116. /**
  117. * Handles the callback logic for this hook.
  118. * @param {ChunkCombination} c combination
  119. * @returns {number} position difference
  120. */
  121. (c) => c.bIdx - c.aIdx,
  122. /**
  123. * Handles the callback logic for this hook.
  124. * @param {number} a a
  125. * @param {number} b b
  126. * @returns {number} result
  127. */
  128. (a, b) => a - b,
  129. // Layer 4: ordered by position in orderedChunk (-> to be deterministic)
  130. /**
  131. * Handles the callback logic for this hook.
  132. * @param {ChunkCombination} a a
  133. * @param {ChunkCombination} b b
  134. * @returns {number} result
  135. */
  136. (a, b) => a.bIdx - b.bIdx
  137. );
  138. // we keep a mapping from chunk to all combinations
  139. // but this mapping is not kept up-to-date with deletions
  140. // so `deleted` flag need to be considered when iterating this
  141. /** @type {Map<Chunk, Set<ChunkCombination>>} */
  142. const combinationsByChunk = new Map();
  143. for (const [bIdx, b] of orderedChunks.entries()) {
  144. // create combination pairs with size and integrated size
  145. for (let aIdx = 0; aIdx < bIdx; aIdx++) {
  146. const a = orderedChunks[aIdx];
  147. // filter pairs that can not be integrated!
  148. if (!chunkGraph.canChunksBeIntegrated(a, b)) continue;
  149. const integratedSize = chunkGraph.getIntegratedChunksSize(
  150. a,
  151. b,
  152. this.options
  153. );
  154. const aSize = chunkGraph.getChunkSize(a, this.options);
  155. const bSize = chunkGraph.getChunkSize(b, this.options);
  156. /** @type {ChunkCombination} */
  157. const c = {
  158. deleted: false,
  159. sizeDiff: aSize + bSize - integratedSize,
  160. integratedSize,
  161. a,
  162. b,
  163. aIdx,
  164. bIdx,
  165. aSize,
  166. bSize
  167. };
  168. combinations.add(c);
  169. addToSetMap(combinationsByChunk, a, c);
  170. addToSetMap(combinationsByChunk, b, c);
  171. }
  172. }
  173. // list of modified chunks during this run
  174. // combinations affected by this change are skipped to allow
  175. // further optimizations
  176. /** @type {Set<Chunk>} */
  177. const modifiedChunks = new Set();
  178. let changed = false;
  179. loop: while (true) {
  180. const combination = combinations.popFirst();
  181. if (combination === undefined) break;
  182. combination.deleted = true;
  183. const { a, b, integratedSize } = combination;
  184. // skip over pair when
  185. // one of the already merged chunks is a parent of one of the chunks
  186. if (modifiedChunks.size > 0) {
  187. const queue = new Set(a.groupsIterable);
  188. for (const group of b.groupsIterable) {
  189. queue.add(group);
  190. }
  191. for (const group of queue) {
  192. for (const mChunk of modifiedChunks) {
  193. if (mChunk !== a && mChunk !== b && mChunk.isInGroup(group)) {
  194. // This is a potential pair which needs recalculation
  195. // We can't do that now, but it merge before following pairs
  196. // so we leave space for it, and consider chunks as modified
  197. // just for the worse case
  198. remainingChunksToMerge--;
  199. if (remainingChunksToMerge <= 0) break loop;
  200. modifiedChunks.add(a);
  201. modifiedChunks.add(b);
  202. continue loop;
  203. }
  204. }
  205. for (const parent of group.parentsIterable) {
  206. queue.add(parent);
  207. }
  208. }
  209. }
  210. // merge the chunks
  211. if (chunkGraph.canChunksBeIntegrated(a, b)) {
  212. chunkGraph.integrateChunks(a, b);
  213. compilation.chunks.delete(b);
  214. // flag chunk a as modified as further optimization are possible for all children here
  215. modifiedChunks.add(a);
  216. changed = true;
  217. remainingChunksToMerge--;
  218. if (remainingChunksToMerge <= 0) break;
  219. // Update all affected combinations
  220. // delete all combination with the removed chunk
  221. // we will use combinations with the kept chunk instead
  222. for (const combination of /** @type {Set<ChunkCombination>} */ (
  223. combinationsByChunk.get(a)
  224. )) {
  225. if (combination.deleted) continue;
  226. combination.deleted = true;
  227. combinations.delete(combination);
  228. }
  229. // Update combinations with the kept chunk with new sizes
  230. for (const combination of /** @type {Set<ChunkCombination>} */ (
  231. combinationsByChunk.get(b)
  232. )) {
  233. if (combination.deleted) continue;
  234. if (combination.a === b) {
  235. if (!chunkGraph.canChunksBeIntegrated(a, combination.b)) {
  236. combination.deleted = true;
  237. combinations.delete(combination);
  238. continue;
  239. }
  240. // Update size
  241. const newIntegratedSize = chunkGraph.getIntegratedChunksSize(
  242. a,
  243. combination.b,
  244. this.options
  245. );
  246. const finishUpdate = combinations.startUpdate(combination);
  247. combination.a = a;
  248. combination.integratedSize = newIntegratedSize;
  249. combination.aSize = integratedSize;
  250. combination.sizeDiff =
  251. combination.bSize + integratedSize - newIntegratedSize;
  252. finishUpdate();
  253. } else if (combination.b === b) {
  254. if (!chunkGraph.canChunksBeIntegrated(combination.a, a)) {
  255. combination.deleted = true;
  256. combinations.delete(combination);
  257. continue;
  258. }
  259. // Update size
  260. const newIntegratedSize = chunkGraph.getIntegratedChunksSize(
  261. combination.a,
  262. a,
  263. this.options
  264. );
  265. const finishUpdate = combinations.startUpdate(combination);
  266. combination.b = a;
  267. combination.integratedSize = newIntegratedSize;
  268. combination.bSize = integratedSize;
  269. combination.sizeDiff =
  270. integratedSize + combination.aSize - newIntegratedSize;
  271. finishUpdate();
  272. }
  273. }
  274. combinationsByChunk.set(
  275. a,
  276. /** @type {Set<ChunkCombination>} */ (
  277. combinationsByChunk.get(b)
  278. )
  279. );
  280. combinationsByChunk.delete(b);
  281. }
  282. }
  283. if (changed) return true;
  284. }
  285. );
  286. });
  287. }
  288. }
  289. module.exports = LimitChunkCountPlugin;