LimitChunkCountPlugin.js 9.2 KB

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