LimitChunkCountPlugin.js 9.7 KB

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