You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

231 lines
7.4 KiB

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