AggressiveSplittingPlugin.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const identifierUtils = require("../util/identifier");
  7. const { intersect } = require("../util/SetHelpers");
  8. const validateOptions = require("schema-utils");
  9. const schema = require("../../schemas/plugins/optimize/AggressiveSplittingPlugin.json");
  10. const moveModuleBetween = (oldChunk, newChunk) => {
  11. return module => {
  12. oldChunk.moveModule(module, newChunk);
  13. };
  14. };
  15. const isNotAEntryModule = entryModule => {
  16. return module => {
  17. return entryModule !== module;
  18. };
  19. };
  20. class AggressiveSplittingPlugin {
  21. constructor(options) {
  22. validateOptions(schema, options || {}, "Aggressive Splitting Plugin");
  23. this.options = options || {};
  24. if (typeof this.options.minSize !== "number") {
  25. this.options.minSize = 30 * 1024;
  26. }
  27. if (typeof this.options.maxSize !== "number") {
  28. this.options.maxSize = 50 * 1024;
  29. }
  30. if (typeof this.options.chunkOverhead !== "number") {
  31. this.options.chunkOverhead = 0;
  32. }
  33. if (typeof this.options.entryChunkMultiplicator !== "number") {
  34. this.options.entryChunkMultiplicator = 1;
  35. }
  36. }
  37. apply(compiler) {
  38. compiler.hooks.thisCompilation.tap(
  39. "AggressiveSplittingPlugin",
  40. compilation => {
  41. let needAdditionalSeal = false;
  42. let newSplits;
  43. let fromAggressiveSplittingSet;
  44. let chunkSplitDataMap;
  45. compilation.hooks.optimize.tap("AggressiveSplittingPlugin", () => {
  46. newSplits = [];
  47. fromAggressiveSplittingSet = new Set();
  48. chunkSplitDataMap = new Map();
  49. });
  50. compilation.hooks.optimizeChunksAdvanced.tap(
  51. "AggressiveSplittingPlugin",
  52. chunks => {
  53. // Precompute stuff
  54. const nameToModuleMap = new Map();
  55. const moduleToNameMap = new Map();
  56. for (const m of compilation.modules) {
  57. const name = identifierUtils.makePathsRelative(
  58. compiler.context,
  59. m.identifier(),
  60. compilation.cache
  61. );
  62. nameToModuleMap.set(name, m);
  63. moduleToNameMap.set(m, name);
  64. }
  65. // Check used chunk ids
  66. const usedIds = new Set();
  67. for (const chunk of chunks) {
  68. usedIds.add(chunk.id);
  69. }
  70. const recordedSplits =
  71. (compilation.records && compilation.records.aggressiveSplits) ||
  72. [];
  73. const usedSplits = newSplits
  74. ? recordedSplits.concat(newSplits)
  75. : recordedSplits;
  76. const minSize = this.options.minSize;
  77. const maxSize = this.options.maxSize;
  78. const applySplit = splitData => {
  79. // Cannot split if id is already taken
  80. if (splitData.id !== undefined && usedIds.has(splitData.id)) {
  81. return false;
  82. }
  83. // Get module objects from names
  84. const selectedModules = splitData.modules.map(name =>
  85. nameToModuleMap.get(name)
  86. );
  87. // Does the modules exist at all?
  88. if (!selectedModules.every(Boolean)) return false;
  89. // Check if size matches (faster than waiting for hash)
  90. const size = selectedModules.reduce(
  91. (sum, m) => sum + m.size(),
  92. 0
  93. );
  94. if (size !== splitData.size) return false;
  95. // get chunks with all modules
  96. const selectedChunks = intersect(
  97. selectedModules.map(m => new Set(m.chunksIterable))
  98. );
  99. // No relevant chunks found
  100. if (selectedChunks.size === 0) return false;
  101. // The found chunk is already the split or similar
  102. if (
  103. selectedChunks.size === 1 &&
  104. Array.from(selectedChunks)[0].getNumberOfModules() ===
  105. selectedModules.length
  106. ) {
  107. const chunk = Array.from(selectedChunks)[0];
  108. if (fromAggressiveSplittingSet.has(chunk)) return false;
  109. fromAggressiveSplittingSet.add(chunk);
  110. chunkSplitDataMap.set(chunk, splitData);
  111. return true;
  112. }
  113. // split the chunk into two parts
  114. const newChunk = compilation.addChunk();
  115. newChunk.chunkReason = "aggressive splitted";
  116. for (const chunk of selectedChunks) {
  117. selectedModules.forEach(moveModuleBetween(chunk, newChunk));
  118. chunk.split(newChunk);
  119. chunk.name = null;
  120. }
  121. fromAggressiveSplittingSet.add(newChunk);
  122. chunkSplitDataMap.set(newChunk, splitData);
  123. if (splitData.id !== null && splitData.id !== undefined) {
  124. newChunk.id = splitData.id;
  125. }
  126. return true;
  127. };
  128. // try to restore to recorded splitting
  129. let changed = false;
  130. for (let j = 0; j < usedSplits.length; j++) {
  131. const splitData = usedSplits[j];
  132. if (applySplit(splitData)) changed = true;
  133. }
  134. // for any chunk which isn't splitted yet, split it and create a new entry
  135. // start with the biggest chunk
  136. const sortedChunks = chunks.slice().sort((a, b) => {
  137. const diff1 = b.modulesSize() - a.modulesSize();
  138. if (diff1) return diff1;
  139. const diff2 = a.getNumberOfModules() - b.getNumberOfModules();
  140. if (diff2) return diff2;
  141. const modulesA = Array.from(a.modulesIterable);
  142. const modulesB = Array.from(b.modulesIterable);
  143. modulesA.sort();
  144. modulesB.sort();
  145. const aI = modulesA[Symbol.iterator]();
  146. const bI = modulesB[Symbol.iterator]();
  147. // eslint-disable-next-line no-constant-condition
  148. while (true) {
  149. const aItem = aI.next();
  150. const bItem = bI.next();
  151. if (aItem.done) return 0;
  152. const aModuleIdentifier = aItem.value.identifier();
  153. const bModuleIdentifier = bItem.value.identifier();
  154. if (aModuleIdentifier > bModuleIdentifier) return -1;
  155. if (aModuleIdentifier < bModuleIdentifier) return 1;
  156. }
  157. });
  158. for (const chunk of sortedChunks) {
  159. if (fromAggressiveSplittingSet.has(chunk)) continue;
  160. const size = chunk.modulesSize();
  161. if (size > maxSize && chunk.getNumberOfModules() > 1) {
  162. const modules = chunk
  163. .getModules()
  164. .filter(isNotAEntryModule(chunk.entryModule))
  165. .sort((a, b) => {
  166. a = a.identifier();
  167. b = b.identifier();
  168. if (a > b) return 1;
  169. if (a < b) return -1;
  170. return 0;
  171. });
  172. const selectedModules = [];
  173. let selectedModulesSize = 0;
  174. for (let k = 0; k < modules.length; k++) {
  175. const module = modules[k];
  176. const newSize = selectedModulesSize + module.size();
  177. if (newSize > maxSize && selectedModulesSize >= minSize) {
  178. break;
  179. }
  180. selectedModulesSize = newSize;
  181. selectedModules.push(module);
  182. }
  183. if (selectedModules.length === 0) continue;
  184. const splitData = {
  185. modules: selectedModules
  186. .map(m => moduleToNameMap.get(m))
  187. .sort(),
  188. size: selectedModulesSize
  189. };
  190. if (applySplit(splitData)) {
  191. newSplits = (newSplits || []).concat(splitData);
  192. changed = true;
  193. }
  194. }
  195. }
  196. if (changed) return true;
  197. }
  198. );
  199. compilation.hooks.recordHash.tap(
  200. "AggressiveSplittingPlugin",
  201. records => {
  202. // 4. save made splittings to records
  203. const allSplits = new Set();
  204. const invalidSplits = new Set();
  205. // Check if some splittings are invalid
  206. // We remove invalid splittings and try again
  207. for (const chunk of compilation.chunks) {
  208. const splitData = chunkSplitDataMap.get(chunk);
  209. if (splitData !== undefined) {
  210. if (splitData.hash && chunk.hash !== splitData.hash) {
  211. // Split was successful, but hash doesn't equal
  212. // We can throw away the split since it's useless now
  213. invalidSplits.add(splitData);
  214. }
  215. }
  216. }
  217. if (invalidSplits.size > 0) {
  218. records.aggressiveSplits = records.aggressiveSplits.filter(
  219. splitData => !invalidSplits.has(splitData)
  220. );
  221. needAdditionalSeal = true;
  222. } else {
  223. // set hash and id values on all (new) splittings
  224. for (const chunk of compilation.chunks) {
  225. const splitData = chunkSplitDataMap.get(chunk);
  226. if (splitData !== undefined) {
  227. splitData.hash = chunk.hash;
  228. splitData.id = chunk.id;
  229. allSplits.add(splitData);
  230. // set flag for stats
  231. chunk.recorded = true;
  232. }
  233. }
  234. // Also add all unused historial splits (after the used ones)
  235. // They can still be used in some future compilation
  236. const recordedSplits =
  237. compilation.records && compilation.records.aggressiveSplits;
  238. if (recordedSplits) {
  239. for (const splitData of recordedSplits) {
  240. if (!invalidSplits.has(splitData)) allSplits.add(splitData);
  241. }
  242. }
  243. // record all splits
  244. records.aggressiveSplits = Array.from(allSplits);
  245. needAdditionalSeal = false;
  246. }
  247. }
  248. );
  249. compilation.hooks.needAdditionalSeal.tap(
  250. "AggressiveSplittingPlugin",
  251. () => {
  252. if (needAdditionalSeal) {
  253. needAdditionalSeal = false;
  254. return true;
  255. }
  256. }
  257. );
  258. }
  259. );
  260. }
  261. }
  262. module.exports = AggressiveSplittingPlugin;