LimitChunkCountPlugin.js 8.2 KB

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