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