UNPKG

40.6 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5
6"use strict";
7
8const FileSystemInfo = require("../FileSystemInfo");
9const ProgressPlugin = require("../ProgressPlugin");
10const { formatSize } = require("../SizeFormatHelpers");
11const SerializerMiddleware = require("../serialization/SerializerMiddleware");
12const LazySet = require("../util/LazySet");
13const makeSerializable = require("../util/makeSerializable");
14const memoize = require("../util/memoize");
15const {
16 createFileSerializer,
17 NOT_SERIALIZABLE
18} = require("../util/serialization");
19
20/** @typedef {import("../../declarations/WebpackOptions").SnapshotOptions} SnapshotOptions */
21/** @typedef {import("../Cache").Etag} Etag */
22/** @typedef {import("../Compiler")} Compiler */
23/** @typedef {import("../FileSystemInfo").Snapshot} Snapshot */
24/** @typedef {import("../logging/Logger").Logger} Logger */
25/** @typedef {import("../util/fs").IntermediateFileSystem} IntermediateFileSystem */
26
27class PackContainer {
28 /**
29 * @param {Object} data stored data
30 * @param {string} version version identifier
31 * @param {Snapshot} buildSnapshot snapshot of all build dependencies
32 * @param {Set<string>} buildDependencies list of all unresolved build dependencies captured
33 * @param {Map<string, string | false>} resolveResults result of the resolved build dependencies
34 * @param {Snapshot} resolveBuildDependenciesSnapshot snapshot of the dependencies of the build dependencies resolving
35 */
36 constructor(
37 data,
38 version,
39 buildSnapshot,
40 buildDependencies,
41 resolveResults,
42 resolveBuildDependenciesSnapshot
43 ) {
44 this.data = data;
45 this.version = version;
46 this.buildSnapshot = buildSnapshot;
47 this.buildDependencies = buildDependencies;
48 this.resolveResults = resolveResults;
49 this.resolveBuildDependenciesSnapshot = resolveBuildDependenciesSnapshot;
50 }
51
52 serialize({ write, writeLazy }) {
53 write(this.version);
54 write(this.buildSnapshot);
55 write(this.buildDependencies);
56 write(this.resolveResults);
57 write(this.resolveBuildDependenciesSnapshot);
58 writeLazy(this.data);
59 }
60
61 deserialize({ read }) {
62 this.version = read();
63 this.buildSnapshot = read();
64 this.buildDependencies = read();
65 this.resolveResults = read();
66 this.resolveBuildDependenciesSnapshot = read();
67 this.data = read();
68 }
69}
70
71makeSerializable(
72 PackContainer,
73 "webpack/lib/cache/PackFileCacheStrategy",
74 "PackContainer"
75);
76
77const MIN_CONTENT_SIZE = 1024 * 1024; // 1 MB
78const CONTENT_COUNT_TO_MERGE = 10;
79const MIN_ITEMS_IN_FRESH_PACK = 100;
80const MAX_ITEMS_IN_FRESH_PACK = 50000;
81const MAX_TIME_IN_FRESH_PACK = 1 * 60 * 1000; // 1 min
82
83class PackItemInfo {
84 /**
85 * @param {string} identifier identifier of item
86 * @param {string | null} etag etag of item
87 * @param {any} value fresh value of item
88 */
89 constructor(identifier, etag, value) {
90 this.identifier = identifier;
91 this.etag = etag;
92 this.location = -1;
93 this.lastAccess = Date.now();
94 this.freshValue = value;
95 }
96}
97
98class Pack {
99 constructor(logger, maxAge) {
100 /** @type {Map<string, PackItemInfo>} */
101 this.itemInfo = new Map();
102 /** @type {string[]} */
103 this.requests = [];
104 this.requestsTimeout = undefined;
105 /** @type {Map<string, PackItemInfo>} */
106 this.freshContent = new Map();
107 /** @type {(undefined | PackContent)[]} */
108 this.content = [];
109 this.invalid = false;
110 this.logger = logger;
111 this.maxAge = maxAge;
112 }
113
114 _addRequest(identifier) {
115 this.requests.push(identifier);
116 if (this.requestsTimeout === undefined) {
117 this.requestsTimeout = setTimeout(() => {
118 this.requests.push(undefined);
119 this.requestsTimeout = undefined;
120 }, MAX_TIME_IN_FRESH_PACK);
121 if (this.requestsTimeout.unref) this.requestsTimeout.unref();
122 }
123 }
124
125 stopCapturingRequests() {
126 if (this.requestsTimeout !== undefined) {
127 clearTimeout(this.requestsTimeout);
128 this.requestsTimeout = undefined;
129 }
130 }
131
132 /**
133 * @param {string} identifier unique name for the resource
134 * @param {string | null} etag etag of the resource
135 * @returns {any} cached content
136 */
137 get(identifier, etag) {
138 const info = this.itemInfo.get(identifier);
139 this._addRequest(identifier);
140 if (info === undefined) {
141 return undefined;
142 }
143 if (info.etag !== etag) return null;
144 info.lastAccess = Date.now();
145 const loc = info.location;
146 if (loc === -1) {
147 return info.freshValue;
148 } else {
149 if (!this.content[loc]) {
150 return undefined;
151 }
152 return this.content[loc].get(identifier);
153 }
154 }
155
156 /**
157 * @param {string} identifier unique name for the resource
158 * @param {string | null} etag etag of the resource
159 * @param {any} data cached content
160 * @returns {void}
161 */
162 set(identifier, etag, data) {
163 if (!this.invalid) {
164 this.invalid = true;
165 this.logger.log(`Pack got invalid because of write to: ${identifier}`);
166 }
167 const info = this.itemInfo.get(identifier);
168 if (info === undefined) {
169 const newInfo = new PackItemInfo(identifier, etag, data);
170 this.itemInfo.set(identifier, newInfo);
171 this._addRequest(identifier);
172 this.freshContent.set(identifier, newInfo);
173 } else {
174 const loc = info.location;
175 if (loc >= 0) {
176 this._addRequest(identifier);
177 this.freshContent.set(identifier, info);
178 const content = this.content[loc];
179 content.delete(identifier);
180 if (content.items.size === 0) {
181 this.content[loc] = undefined;
182 this.logger.debug("Pack %d got empty and is removed", loc);
183 }
184 }
185 info.freshValue = data;
186 info.lastAccess = Date.now();
187 info.etag = etag;
188 info.location = -1;
189 }
190 }
191
192 getContentStats() {
193 let count = 0;
194 let size = 0;
195 for (const content of this.content) {
196 if (content !== undefined) {
197 count++;
198 const s = content.getSize();
199 if (s > 0) {
200 size += s;
201 }
202 }
203 }
204 return { count, size };
205 }
206
207 /**
208 * @returns {number} new location of data entries
209 */
210 _findLocation() {
211 let i;
212 for (i = 0; i < this.content.length && this.content[i] !== undefined; i++);
213 return i;
214 }
215
216 _gcAndUpdateLocation(items, usedItems, newLoc) {
217 let count = 0;
218 let lastGC;
219 const now = Date.now();
220 for (const identifier of items) {
221 const info = this.itemInfo.get(identifier);
222 if (now - info.lastAccess > this.maxAge) {
223 this.itemInfo.delete(identifier);
224 items.delete(identifier);
225 usedItems.delete(identifier);
226 count++;
227 lastGC = identifier;
228 } else {
229 info.location = newLoc;
230 }
231 }
232 if (count > 0) {
233 this.logger.log(
234 "Garbage Collected %d old items at pack %d (%d items remaining) e. g. %s",
235 count,
236 newLoc,
237 items.size,
238 lastGC
239 );
240 }
241 }
242
243 _persistFreshContent() {
244 const itemsCount = this.freshContent.size;
245 if (itemsCount > 0) {
246 const packCount = Math.ceil(itemsCount / MAX_ITEMS_IN_FRESH_PACK);
247 const itemsPerPack = Math.ceil(itemsCount / packCount);
248 const packs = [];
249 let i = 0;
250 let ignoreNextTimeTick = false;
251 const createNextPack = () => {
252 const loc = this._findLocation();
253 this.content[loc] = null; // reserve
254 const pack = {
255 /** @type {Set<string>} */
256 items: new Set(),
257 /** @type {Map<string, any>} */
258 map: new Map(),
259 loc
260 };
261 packs.push(pack);
262 return pack;
263 };
264 let pack = createNextPack();
265 if (this.requestsTimeout !== undefined)
266 clearTimeout(this.requestsTimeout);
267 for (const identifier of this.requests) {
268 if (identifier === undefined) {
269 if (ignoreNextTimeTick) {
270 ignoreNextTimeTick = false;
271 } else if (pack.items.size >= MIN_ITEMS_IN_FRESH_PACK) {
272 i = 0;
273 pack = createNextPack();
274 }
275 continue;
276 }
277 const info = this.freshContent.get(identifier);
278 if (info === undefined) continue;
279 pack.items.add(identifier);
280 pack.map.set(identifier, info.freshValue);
281 info.location = pack.loc;
282 info.freshValue = undefined;
283 this.freshContent.delete(identifier);
284 if (++i > itemsPerPack) {
285 i = 0;
286 pack = createNextPack();
287 ignoreNextTimeTick = true;
288 }
289 }
290 this.requests.length = 0;
291 for (const pack of packs) {
292 this.content[pack.loc] = new PackContent(
293 pack.items,
294 new Set(pack.items),
295 new PackContentItems(pack.map)
296 );
297 }
298 this.logger.log(
299 `${itemsCount} fresh items in cache put into pack ${
300 packs.length > 1
301 ? packs
302 .map(pack => `${pack.loc} (${pack.items.size} items)`)
303 .join(", ")
304 : packs[0].loc
305 }`
306 );
307 }
308 }
309
310 /**
311 * Merges small content files to a single content file
312 */
313 _optimizeSmallContent() {
314 // 1. Find all small content files
315 // Treat unused content files separately to avoid
316 // a merge-split cycle
317 /** @type {number[]} */
318 const smallUsedContents = [];
319 /** @type {number} */
320 let smallUsedContentSize = 0;
321 /** @type {number[]} */
322 const smallUnusedContents = [];
323 /** @type {number} */
324 let smallUnusedContentSize = 0;
325 for (let i = 0; i < this.content.length; i++) {
326 const content = this.content[i];
327 if (content === undefined) continue;
328 if (content.outdated) continue;
329 const size = content.getSize();
330 if (size < 0 || size > MIN_CONTENT_SIZE) continue;
331 if (content.used.size > 0) {
332 smallUsedContents.push(i);
333 smallUsedContentSize += size;
334 } else {
335 smallUnusedContents.push(i);
336 smallUnusedContentSize += size;
337 }
338 }
339
340 // 2. Check if minimum number is reached
341 let mergedIndices;
342 if (
343 smallUsedContents.length >= CONTENT_COUNT_TO_MERGE ||
344 smallUsedContentSize > MIN_CONTENT_SIZE
345 ) {
346 mergedIndices = smallUsedContents;
347 } else if (
348 smallUnusedContents.length >= CONTENT_COUNT_TO_MERGE ||
349 smallUnusedContentSize > MIN_CONTENT_SIZE
350 ) {
351 mergedIndices = smallUnusedContents;
352 } else return;
353
354 const mergedContent = [];
355
356 // 3. Remove old content entries
357 for (const i of mergedIndices) {
358 mergedContent.push(this.content[i]);
359 this.content[i] = undefined;
360 }
361
362 // 4. Determine merged items
363 /** @type {Set<string>} */
364 const mergedItems = new Set();
365 /** @type {Set<string>} */
366 const mergedUsedItems = new Set();
367 /** @type {(function(Map<string, any>): Promise)[]} */
368 const addToMergedMap = [];
369 for (const content of mergedContent) {
370 for (const identifier of content.items) {
371 mergedItems.add(identifier);
372 }
373 for (const identifier of content.used) {
374 mergedUsedItems.add(identifier);
375 }
376 addToMergedMap.push(async map => {
377 // unpack existing content
378 // after that values are accessible in .content
379 await content.unpack(
380 "it should be merged with other small pack contents"
381 );
382 for (const [identifier, value] of content.content) {
383 map.set(identifier, value);
384 }
385 });
386 }
387
388 // 5. GC and update location of merged items
389 const newLoc = this._findLocation();
390 this._gcAndUpdateLocation(mergedItems, mergedUsedItems, newLoc);
391
392 // 6. If not empty, store content somewhere
393 if (mergedItems.size > 0) {
394 this.content[newLoc] = new PackContent(
395 mergedItems,
396 mergedUsedItems,
397 memoize(async () => {
398 /** @type {Map<string, any>} */
399 const map = new Map();
400 await Promise.all(addToMergedMap.map(fn => fn(map)));
401 return new PackContentItems(map);
402 })
403 );
404 this.logger.log(
405 "Merged %d small files with %d cache items into pack %d",
406 mergedContent.length,
407 mergedItems.size,
408 newLoc
409 );
410 }
411 }
412
413 /**
414 * Split large content files with used and unused items
415 * into two parts to separate used from unused items
416 */
417 _optimizeUnusedContent() {
418 // 1. Find a large content file with used and unused items
419 for (let i = 0; i < this.content.length; i++) {
420 const content = this.content[i];
421 if (content === undefined) continue;
422 const size = content.getSize();
423 if (size < MIN_CONTENT_SIZE) continue;
424 const used = content.used.size;
425 const total = content.items.size;
426 if (used > 0 && used < total) {
427 // 2. Remove this content
428 this.content[i] = undefined;
429
430 // 3. Determine items for the used content file
431 const usedItems = new Set(content.used);
432 const newLoc = this._findLocation();
433 this._gcAndUpdateLocation(usedItems, usedItems, newLoc);
434
435 // 4. Create content file for used items
436 if (usedItems.size > 0) {
437 this.content[newLoc] = new PackContent(
438 usedItems,
439 new Set(usedItems),
440 async () => {
441 await content.unpack(
442 "it should be splitted into used and unused items"
443 );
444 const map = new Map();
445 for (const identifier of usedItems) {
446 map.set(identifier, content.content.get(identifier));
447 }
448 return new PackContentItems(map);
449 }
450 );
451 }
452
453 // 5. Determine items for the unused content file
454 const unusedItems = new Set(content.items);
455 const usedOfUnusedItems = new Set();
456 for (const identifier of usedItems) {
457 unusedItems.delete(identifier);
458 }
459 const newUnusedLoc = this._findLocation();
460 this._gcAndUpdateLocation(unusedItems, usedOfUnusedItems, newUnusedLoc);
461
462 // 6. Create content file for unused items
463 if (unusedItems.size > 0) {
464 this.content[newUnusedLoc] = new PackContent(
465 unusedItems,
466 usedOfUnusedItems,
467 async () => {
468 await content.unpack(
469 "it should be splitted into used and unused items"
470 );
471 const map = new Map();
472 for (const identifier of unusedItems) {
473 map.set(identifier, content.content.get(identifier));
474 }
475 return new PackContentItems(map);
476 }
477 );
478 }
479
480 this.logger.log(
481 "Split pack %d into pack %d with %d used items and pack %d with %d unused items",
482 i,
483 newLoc,
484 usedItems.size,
485 newUnusedLoc,
486 unusedItems.size
487 );
488
489 // optimizing only one of them is good enough and
490 // reduces the amount of serialization needed
491 return;
492 }
493 }
494 }
495
496 /**
497 * Find the content with the oldest item and run GC on that.
498 * Only runs for one content to avoid large invalidation.
499 */
500 _gcOldestContent() {
501 /** @type {PackItemInfo} */
502 let oldest = undefined;
503 for (const info of this.itemInfo.values()) {
504 if (oldest === undefined || info.lastAccess < oldest.lastAccess) {
505 oldest = info;
506 }
507 }
508 if (Date.now() - oldest.lastAccess > this.maxAge) {
509 const loc = oldest.location;
510 if (loc < 0) return;
511 const content = this.content[loc];
512 const items = new Set(content.items);
513 const usedItems = new Set(content.used);
514 this._gcAndUpdateLocation(items, usedItems, loc);
515
516 this.content[loc] =
517 items.size > 0
518 ? new PackContent(items, usedItems, async () => {
519 await content.unpack(
520 "it contains old items that should be garbage collected"
521 );
522 const map = new Map();
523 for (const identifier of items) {
524 map.set(identifier, content.content.get(identifier));
525 }
526 return new PackContentItems(map);
527 })
528 : undefined;
529 }
530 }
531
532 serialize({ write, writeSeparate }) {
533 this._persistFreshContent();
534 this._optimizeSmallContent();
535 this._optimizeUnusedContent();
536 this._gcOldestContent();
537 for (const identifier of this.itemInfo.keys()) {
538 write(identifier);
539 }
540 write(null); // null as marker of the end of keys
541 for (const info of this.itemInfo.values()) {
542 write(info.etag);
543 }
544 for (const info of this.itemInfo.values()) {
545 write(info.lastAccess);
546 }
547 for (let i = 0; i < this.content.length; i++) {
548 const content = this.content[i];
549 if (content !== undefined) {
550 write(content.items);
551 content.writeLazy(lazy => writeSeparate(lazy, { name: `${i}` }));
552 } else {
553 write(undefined); // undefined marks an empty content slot
554 }
555 }
556 write(null); // null as marker of the end of items
557 }
558
559 deserialize({ read, logger }) {
560 this.logger = logger;
561 {
562 const items = [];
563 let item = read();
564 while (item !== null) {
565 items.push(item);
566 item = read();
567 }
568 this.itemInfo.clear();
569 const infoItems = items.map(identifier => {
570 const info = new PackItemInfo(identifier, undefined, undefined);
571 this.itemInfo.set(identifier, info);
572 return info;
573 });
574 for (const info of infoItems) {
575 info.etag = read();
576 }
577 for (const info of infoItems) {
578 info.lastAccess = read();
579 }
580 }
581 this.content.length = 0;
582 let items = read();
583 while (items !== null) {
584 if (items === undefined) {
585 this.content.push(items);
586 } else {
587 const idx = this.content.length;
588 const lazy = read();
589 this.content.push(
590 new PackContent(
591 items,
592 new Set(),
593 lazy,
594 logger,
595 `${this.content.length}`
596 )
597 );
598 for (const identifier of items) {
599 this.itemInfo.get(identifier).location = idx;
600 }
601 }
602 items = read();
603 }
604 }
605}
606
607makeSerializable(Pack, "webpack/lib/cache/PackFileCacheStrategy", "Pack");
608
609class PackContentItems {
610 /**
611 * @param {Map<string, any>} map items
612 */
613 constructor(map) {
614 this.map = map;
615 }
616
617 serialize({ write, snapshot, rollback, logger, profile }) {
618 if (profile) {
619 write(false);
620 for (const [key, value] of this.map) {
621 const s = snapshot();
622 try {
623 write(key);
624 const start = process.hrtime();
625 write(value);
626 const durationHr = process.hrtime(start);
627 const duration = durationHr[0] * 1000 + durationHr[1] / 1e6;
628 if (duration > 1) {
629 if (duration > 500)
630 logger.error(`Serialization of '${key}': ${duration} ms`);
631 else if (duration > 50)
632 logger.warn(`Serialization of '${key}': ${duration} ms`);
633 else if (duration > 10)
634 logger.info(`Serialization of '${key}': ${duration} ms`);
635 else if (duration > 5)
636 logger.log(`Serialization of '${key}': ${duration} ms`);
637 else logger.debug(`Serialization of '${key}': ${duration} ms`);
638 }
639 } catch (e) {
640 rollback(s);
641 if (e === NOT_SERIALIZABLE) continue;
642 logger.warn(
643 `Skipped not serializable cache item '${key}': ${e.message}`
644 );
645 logger.debug(e.stack);
646 }
647 }
648 write(null);
649 return;
650 }
651 // Try to serialize all at once
652 const s = snapshot();
653 try {
654 write(true);
655 write(this.map);
656 } catch (e) {
657 rollback(s);
658
659 // Try to serialize each item on it's own
660 write(false);
661 for (const [key, value] of this.map) {
662 const s = snapshot();
663 try {
664 write(key);
665 write(value);
666 } catch (e) {
667 rollback(s);
668 if (e === NOT_SERIALIZABLE) continue;
669 logger.warn(
670 `Skipped not serializable cache item '${key}': ${e.message}`
671 );
672 logger.debug(e.stack);
673 }
674 }
675 write(null);
676 }
677 }
678
679 deserialize({ read, logger, profile }) {
680 if (read()) {
681 this.map = read();
682 } else if (profile) {
683 const map = new Map();
684 let key = read();
685 while (key !== null) {
686 const start = process.hrtime();
687 const value = read();
688 const durationHr = process.hrtime(start);
689 const duration = durationHr[0] * 1000 + durationHr[1] / 1e6;
690 if (duration > 1) {
691 if (duration > 100)
692 logger.error(`Deserialization of '${key}': ${duration} ms`);
693 else if (duration > 20)
694 logger.warn(`Deserialization of '${key}': ${duration} ms`);
695 else if (duration > 5)
696 logger.info(`Deserialization of '${key}': ${duration} ms`);
697 else if (duration > 2)
698 logger.log(`Deserialization of '${key}': ${duration} ms`);
699 else logger.debug(`Deserialization of '${key}': ${duration} ms`);
700 }
701 map.set(key, value);
702 key = read();
703 }
704 this.map = map;
705 } else {
706 const map = new Map();
707 let key = read();
708 while (key !== null) {
709 map.set(key, read());
710 key = read();
711 }
712 this.map = map;
713 }
714 }
715}
716
717makeSerializable(
718 PackContentItems,
719 "webpack/lib/cache/PackFileCacheStrategy",
720 "PackContentItems"
721);
722
723class PackContent {
724 /*
725 This class can be in these states:
726 | this.lazy | this.content | this.outdated | state
727 A1 | undefined | Map | false | fresh content
728 A2 | undefined | Map | true | (will not happen)
729 B1 | lazy () => {} | undefined | false | not deserialized
730 B2 | lazy () => {} | undefined | true | not deserialized, but some items has been removed
731 C1 | lazy* () => {} | Map | false | deserialized
732 C2 | lazy* () => {} | Map | true | deserialized, and some items has been removed
733
734 this.used is a subset of this.items.
735 this.items is a subset of this.content.keys() resp. this.lazy().map.keys()
736 When this.outdated === false, this.items === this.content.keys() resp. this.lazy().map.keys()
737 When this.outdated === true, this.items should be used to recreated this.lazy/this.content.
738 When this.lazy and this.content is set, they contain the same data.
739 this.get must only be called with a valid item from this.items.
740 In state C this.lazy is unMemoized
741 */
742
743 /**
744 * @param {Set<string>} items keys
745 * @param {Set<string>} usedItems used keys
746 * @param {PackContentItems | function(): Promise<PackContentItems>} dataOrFn sync or async content
747 * @param {Logger=} logger logger for logging
748 * @param {string=} lazyName name of dataOrFn for logging
749 */
750 constructor(items, usedItems, dataOrFn, logger, lazyName) {
751 this.items = items;
752 /** @type {function(): Promise<PackContentItems> | PackContentItems} */
753 this.lazy = typeof dataOrFn === "function" ? dataOrFn : undefined;
754 /** @type {Map<string, any>} */
755 this.content = typeof dataOrFn === "function" ? undefined : dataOrFn.map;
756 this.outdated = false;
757 this.used = usedItems;
758 this.logger = logger;
759 this.lazyName = lazyName;
760 }
761
762 get(identifier) {
763 this.used.add(identifier);
764 if (this.content) {
765 return this.content.get(identifier);
766 }
767
768 // We are in state B
769 const { lazyName } = this;
770 let timeMessage;
771 if (lazyName) {
772 // only log once
773 this.lazyName = undefined;
774 timeMessage = `restore cache content ${lazyName} (${formatSize(
775 this.getSize()
776 )})`;
777 this.logger.log(
778 `starting to restore cache content ${lazyName} (${formatSize(
779 this.getSize()
780 )}) because of request to: ${identifier}`
781 );
782 this.logger.time(timeMessage);
783 }
784 const value = this.lazy();
785 if ("then" in value) {
786 return value.then(data => {
787 const map = data.map;
788 if (timeMessage) {
789 this.logger.timeEnd(timeMessage);
790 }
791 // Move to state C
792 this.content = map;
793 this.lazy = SerializerMiddleware.unMemoizeLazy(this.lazy);
794 return map.get(identifier);
795 });
796 } else {
797 const map = value.map;
798 if (timeMessage) {
799 this.logger.timeEnd(timeMessage);
800 }
801 // Move to state C
802 this.content = map;
803 this.lazy = SerializerMiddleware.unMemoizeLazy(this.lazy);
804 return map.get(identifier);
805 }
806 }
807
808 /**
809 * @param {string} reason explanation why unpack is necessary
810 * @returns {void | Promise} maybe a promise if lazy
811 */
812 unpack(reason) {
813 if (this.content) return;
814
815 // Move from state B to C
816 if (this.lazy) {
817 const { lazyName } = this;
818 let timeMessage;
819 if (lazyName) {
820 // only log once
821 this.lazyName = undefined;
822 timeMessage = `unpack cache content ${lazyName} (${formatSize(
823 this.getSize()
824 )})`;
825 this.logger.log(
826 `starting to unpack cache content ${lazyName} (${formatSize(
827 this.getSize()
828 )}) because ${reason}`
829 );
830 this.logger.time(timeMessage);
831 }
832 const value = this.lazy();
833 if ("then" in value) {
834 return value.then(data => {
835 if (timeMessage) {
836 this.logger.timeEnd(timeMessage);
837 }
838 this.content = data.map;
839 });
840 } else {
841 if (timeMessage) {
842 this.logger.timeEnd(timeMessage);
843 }
844 this.content = value.map;
845 }
846 }
847 }
848
849 /**
850 * @returns {number} size of the content or -1 if not known
851 */
852 getSize() {
853 if (!this.lazy) return -1;
854 const options = /** @type {any} */ (this.lazy).options;
855 if (!options) return -1;
856 const size = options.size;
857 if (typeof size !== "number") return -1;
858 return size;
859 }
860
861 delete(identifier) {
862 this.items.delete(identifier);
863 this.used.delete(identifier);
864 this.outdated = true;
865 }
866
867 /**
868 * @template T
869 * @param {function(any): function(): Promise<PackContentItems> | PackContentItems} write write function
870 * @returns {void}
871 */
872 writeLazy(write) {
873 if (!this.outdated && this.lazy) {
874 // State B1 or C1
875 // this.lazy is still the valid deserialized version
876 write(this.lazy);
877 return;
878 }
879 if (!this.outdated && this.content) {
880 // State A1
881 const map = new Map(this.content);
882 // Move to state C1
883 this.lazy = SerializerMiddleware.unMemoizeLazy(
884 write(() => new PackContentItems(map))
885 );
886 return;
887 }
888 if (this.content) {
889 // State A2 or C2
890 /** @type {Map<string, any>} */
891 const map = new Map();
892 for (const item of this.items) {
893 map.set(item, this.content.get(item));
894 }
895 // Move to state C1
896 this.outdated = false;
897 this.content = map;
898 this.lazy = SerializerMiddleware.unMemoizeLazy(
899 write(() => new PackContentItems(map))
900 );
901 return;
902 }
903 // State B2
904 const { lazyName } = this;
905 let timeMessage;
906 if (lazyName) {
907 // only log once
908 this.lazyName = undefined;
909 timeMessage = `unpack cache content ${lazyName} (${formatSize(
910 this.getSize()
911 )})`;
912 this.logger.log(
913 `starting to unpack cache content ${lazyName} (${formatSize(
914 this.getSize()
915 )}) because it's outdated and need to be serialized`
916 );
917 this.logger.time(timeMessage);
918 }
919 const value = this.lazy();
920 this.outdated = false;
921 if ("then" in value) {
922 // Move to state B1
923 this.lazy = write(() =>
924 value.then(data => {
925 if (timeMessage) {
926 this.logger.timeEnd(timeMessage);
927 }
928 const oldMap = data.map;
929 /** @type {Map<string, any>} */
930 const map = new Map();
931 for (const item of this.items) {
932 map.set(item, oldMap.get(item));
933 }
934 // Move to state C1 (or maybe C2)
935 this.content = map;
936 this.lazy = SerializerMiddleware.unMemoizeLazy(this.lazy);
937
938 return new PackContentItems(map);
939 })
940 );
941 } else {
942 // Move to state C1
943 if (timeMessage) {
944 this.logger.timeEnd(timeMessage);
945 }
946 const oldMap = value.map;
947 /** @type {Map<string, any>} */
948 const map = new Map();
949 for (const item of this.items) {
950 map.set(item, oldMap.get(item));
951 }
952 this.content = map;
953 this.lazy = write(() => new PackContentItems(map));
954 }
955 }
956}
957
958const allowCollectingMemory = buf => {
959 const wasted = buf.buffer.byteLength - buf.byteLength;
960 if (wasted > 8192 && (wasted > 1048576 || wasted > buf.byteLength)) {
961 return Buffer.from(buf);
962 }
963 return buf;
964};
965
966class PackFileCacheStrategy {
967 /**
968 * @param {Object} options options
969 * @param {Compiler} options.compiler the compiler
970 * @param {IntermediateFileSystem} options.fs the filesystem
971 * @param {string} options.context the context directory
972 * @param {string} options.cacheLocation the location of the cache data
973 * @param {string} options.version version identifier
974 * @param {Logger} options.logger a logger
975 * @param {SnapshotOptions} options.snapshot options regarding snapshotting
976 * @param {number} options.maxAge max age of cache items
977 * @param {boolean} options.profile track and log detailed timing information for individual cache items
978 * @param {boolean} options.allowCollectingMemory allow to collect unused memory created during deserialization
979 * @param {false | "gzip" | "brotli"} options.compression compression used
980 */
981 constructor({
982 compiler,
983 fs,
984 context,
985 cacheLocation,
986 version,
987 logger,
988 snapshot,
989 maxAge,
990 profile,
991 allowCollectingMemory,
992 compression
993 }) {
994 this.fileSerializer = createFileSerializer(
995 fs,
996 compiler.options.output.hashFunction
997 );
998 this.fileSystemInfo = new FileSystemInfo(fs, {
999 managedPaths: snapshot.managedPaths,
1000 immutablePaths: snapshot.immutablePaths,
1001 logger: logger.getChildLogger("webpack.FileSystemInfo"),
1002 hashFunction: compiler.options.output.hashFunction
1003 });
1004 this.compiler = compiler;
1005 this.context = context;
1006 this.cacheLocation = cacheLocation;
1007 this.version = version;
1008 this.logger = logger;
1009 this.maxAge = maxAge;
1010 this.profile = profile;
1011 this.allowCollectingMemory = allowCollectingMemory;
1012 this.compression = compression;
1013 this._extension =
1014 compression === "brotli"
1015 ? ".pack.br"
1016 : compression === "gzip"
1017 ? ".pack.gz"
1018 : ".pack";
1019 this.snapshot = snapshot;
1020 /** @type {Set<string>} */
1021 this.buildDependencies = new Set();
1022 /** @type {LazySet<string>} */
1023 this.newBuildDependencies = new LazySet();
1024 /** @type {Snapshot} */
1025 this.resolveBuildDependenciesSnapshot = undefined;
1026 /** @type {Map<string, string | false>} */
1027 this.resolveResults = undefined;
1028 /** @type {Snapshot} */
1029 this.buildSnapshot = undefined;
1030 /** @type {Promise<Pack>} */
1031 this.packPromise = this._openPack();
1032 this.storePromise = Promise.resolve();
1033 }
1034
1035 _getPack() {
1036 if (this.packPromise === undefined) {
1037 this.packPromise = this.storePromise.then(() => this._openPack());
1038 }
1039 return this.packPromise;
1040 }
1041
1042 /**
1043 * @returns {Promise<Pack>} the pack
1044 */
1045 _openPack() {
1046 const { logger, profile, cacheLocation, version } = this;
1047 /** @type {Snapshot} */
1048 let buildSnapshot;
1049 /** @type {Set<string>} */
1050 let buildDependencies;
1051 /** @type {Set<string>} */
1052 let newBuildDependencies;
1053 /** @type {Snapshot} */
1054 let resolveBuildDependenciesSnapshot;
1055 /** @type {Map<string, string | false>} */
1056 let resolveResults;
1057 logger.time("restore cache container");
1058 return this.fileSerializer
1059 .deserialize(null, {
1060 filename: `${cacheLocation}/index${this._extension}`,
1061 extension: `${this._extension}`,
1062 logger,
1063 profile,
1064 retainedBuffer: this.allowCollectingMemory
1065 ? allowCollectingMemory
1066 : undefined
1067 })
1068 .catch(err => {
1069 if (err.code !== "ENOENT") {
1070 logger.warn(
1071 `Restoring pack failed from ${cacheLocation}${this._extension}: ${err}`
1072 );
1073 logger.debug(err.stack);
1074 } else {
1075 logger.debug(
1076 `No pack exists at ${cacheLocation}${this._extension}: ${err}`
1077 );
1078 }
1079 return undefined;
1080 })
1081 .then(packContainer => {
1082 logger.timeEnd("restore cache container");
1083 if (!packContainer) return undefined;
1084 if (!(packContainer instanceof PackContainer)) {
1085 logger.warn(
1086 `Restored pack from ${cacheLocation}${this._extension}, but contained content is unexpected.`,
1087 packContainer
1088 );
1089 return undefined;
1090 }
1091 if (packContainer.version !== version) {
1092 logger.log(
1093 `Restored pack from ${cacheLocation}${this._extension}, but version doesn't match.`
1094 );
1095 return undefined;
1096 }
1097 logger.time("check build dependencies");
1098 return Promise.all([
1099 new Promise((resolve, reject) => {
1100 this.fileSystemInfo.checkSnapshotValid(
1101 packContainer.buildSnapshot,
1102 (err, valid) => {
1103 if (err) {
1104 logger.log(
1105 `Restored pack from ${cacheLocation}${this._extension}, but checking snapshot of build dependencies errored: ${err}.`
1106 );
1107 logger.debug(err.stack);
1108 return resolve(false);
1109 }
1110 if (!valid) {
1111 logger.log(
1112 `Restored pack from ${cacheLocation}${this._extension}, but build dependencies have changed.`
1113 );
1114 return resolve(false);
1115 }
1116 buildSnapshot = packContainer.buildSnapshot;
1117 return resolve(true);
1118 }
1119 );
1120 }),
1121 new Promise((resolve, reject) => {
1122 this.fileSystemInfo.checkSnapshotValid(
1123 packContainer.resolveBuildDependenciesSnapshot,
1124 (err, valid) => {
1125 if (err) {
1126 logger.log(
1127 `Restored pack from ${cacheLocation}${this._extension}, but checking snapshot of resolving of build dependencies errored: ${err}.`
1128 );
1129 logger.debug(err.stack);
1130 return resolve(false);
1131 }
1132 if (valid) {
1133 resolveBuildDependenciesSnapshot =
1134 packContainer.resolveBuildDependenciesSnapshot;
1135 buildDependencies = packContainer.buildDependencies;
1136 resolveResults = packContainer.resolveResults;
1137 return resolve(true);
1138 }
1139 logger.log(
1140 "resolving of build dependencies is invalid, will re-resolve build dependencies"
1141 );
1142 this.fileSystemInfo.checkResolveResultsValid(
1143 packContainer.resolveResults,
1144 (err, valid) => {
1145 if (err) {
1146 logger.log(
1147 `Restored pack from ${cacheLocation}${this._extension}, but resolving of build dependencies errored: ${err}.`
1148 );
1149 logger.debug(err.stack);
1150 return resolve(false);
1151 }
1152 if (valid) {
1153 newBuildDependencies = packContainer.buildDependencies;
1154 resolveResults = packContainer.resolveResults;
1155 return resolve(true);
1156 }
1157 logger.log(
1158 `Restored pack from ${cacheLocation}${this._extension}, but build dependencies resolve to different locations.`
1159 );
1160 return resolve(false);
1161 }
1162 );
1163 }
1164 );
1165 })
1166 ])
1167 .catch(err => {
1168 logger.timeEnd("check build dependencies");
1169 throw err;
1170 })
1171 .then(([buildSnapshotValid, resolveValid]) => {
1172 logger.timeEnd("check build dependencies");
1173 if (buildSnapshotValid && resolveValid) {
1174 logger.time("restore cache content metadata");
1175 const d = packContainer.data();
1176 logger.timeEnd("restore cache content metadata");
1177 return d;
1178 }
1179 return undefined;
1180 });
1181 })
1182 .then(pack => {
1183 if (pack) {
1184 pack.maxAge = this.maxAge;
1185 this.buildSnapshot = buildSnapshot;
1186 if (buildDependencies) this.buildDependencies = buildDependencies;
1187 if (newBuildDependencies)
1188 this.newBuildDependencies.addAll(newBuildDependencies);
1189 this.resolveResults = resolveResults;
1190 this.resolveBuildDependenciesSnapshot =
1191 resolveBuildDependenciesSnapshot;
1192 return pack;
1193 }
1194 return new Pack(logger, this.maxAge);
1195 })
1196 .catch(err => {
1197 this.logger.warn(
1198 `Restoring pack from ${cacheLocation}${this._extension} failed: ${err}`
1199 );
1200 this.logger.debug(err.stack);
1201 return new Pack(logger, this.maxAge);
1202 });
1203 }
1204
1205 /**
1206 * @param {string} identifier unique name for the resource
1207 * @param {Etag | null} etag etag of the resource
1208 * @param {any} data cached content
1209 * @returns {Promise<void>} promise
1210 */
1211 store(identifier, etag, data) {
1212 return this._getPack().then(pack => {
1213 pack.set(identifier, etag === null ? null : etag.toString(), data);
1214 });
1215 }
1216
1217 /**
1218 * @param {string} identifier unique name for the resource
1219 * @param {Etag | null} etag etag of the resource
1220 * @returns {Promise<any>} promise to the cached content
1221 */
1222 restore(identifier, etag) {
1223 return this._getPack()
1224 .then(pack =>
1225 pack.get(identifier, etag === null ? null : etag.toString())
1226 )
1227 .catch(err => {
1228 if (err && err.code !== "ENOENT") {
1229 this.logger.warn(
1230 `Restoring failed for ${identifier} from pack: ${err}`
1231 );
1232 this.logger.debug(err.stack);
1233 }
1234 });
1235 }
1236
1237 storeBuildDependencies(dependencies) {
1238 this.newBuildDependencies.addAll(dependencies);
1239 }
1240
1241 afterAllStored() {
1242 const packPromise = this.packPromise;
1243 if (packPromise === undefined) return Promise.resolve();
1244 const reportProgress = ProgressPlugin.getReporter(this.compiler);
1245 return (this.storePromise = packPromise
1246 .then(pack => {
1247 pack.stopCapturingRequests();
1248 if (!pack.invalid) return;
1249 this.packPromise = undefined;
1250 this.logger.log(`Storing pack...`);
1251 let promise;
1252 const newBuildDependencies = new Set();
1253 for (const dep of this.newBuildDependencies) {
1254 if (!this.buildDependencies.has(dep)) {
1255 newBuildDependencies.add(dep);
1256 }
1257 }
1258 if (newBuildDependencies.size > 0 || !this.buildSnapshot) {
1259 if (reportProgress) reportProgress(0.5, "resolve build dependencies");
1260 this.logger.debug(
1261 `Capturing build dependencies... (${Array.from(
1262 newBuildDependencies
1263 ).join(", ")})`
1264 );
1265 promise = new Promise((resolve, reject) => {
1266 this.logger.time("resolve build dependencies");
1267 this.fileSystemInfo.resolveBuildDependencies(
1268 this.context,
1269 newBuildDependencies,
1270 (err, result) => {
1271 this.logger.timeEnd("resolve build dependencies");
1272 if (err) return reject(err);
1273
1274 this.logger.time("snapshot build dependencies");
1275 const {
1276 files,
1277 directories,
1278 missing,
1279 resolveResults,
1280 resolveDependencies
1281 } = result;
1282 if (this.resolveResults) {
1283 for (const [key, value] of resolveResults) {
1284 this.resolveResults.set(key, value);
1285 }
1286 } else {
1287 this.resolveResults = resolveResults;
1288 }
1289 if (reportProgress) {
1290 reportProgress(
1291 0.6,
1292 "snapshot build dependencies",
1293 "resolving"
1294 );
1295 }
1296 this.fileSystemInfo.createSnapshot(
1297 undefined,
1298 resolveDependencies.files,
1299 resolveDependencies.directories,
1300 resolveDependencies.missing,
1301 this.snapshot.resolveBuildDependencies,
1302 (err, snapshot) => {
1303 if (err) {
1304 this.logger.timeEnd("snapshot build dependencies");
1305 return reject(err);
1306 }
1307 if (!snapshot) {
1308 this.logger.timeEnd("snapshot build dependencies");
1309 return reject(
1310 new Error("Unable to snapshot resolve dependencies")
1311 );
1312 }
1313 if (this.resolveBuildDependenciesSnapshot) {
1314 this.resolveBuildDependenciesSnapshot =
1315 this.fileSystemInfo.mergeSnapshots(
1316 this.resolveBuildDependenciesSnapshot,
1317 snapshot
1318 );
1319 } else {
1320 this.resolveBuildDependenciesSnapshot = snapshot;
1321 }
1322 if (reportProgress) {
1323 reportProgress(
1324 0.7,
1325 "snapshot build dependencies",
1326 "modules"
1327 );
1328 }
1329 this.fileSystemInfo.createSnapshot(
1330 undefined,
1331 files,
1332 directories,
1333 missing,
1334 this.snapshot.buildDependencies,
1335 (err, snapshot) => {
1336 this.logger.timeEnd("snapshot build dependencies");
1337 if (err) return reject(err);
1338 if (!snapshot) {
1339 return reject(
1340 new Error("Unable to snapshot build dependencies")
1341 );
1342 }
1343 this.logger.debug("Captured build dependencies");
1344
1345 if (this.buildSnapshot) {
1346 this.buildSnapshot =
1347 this.fileSystemInfo.mergeSnapshots(
1348 this.buildSnapshot,
1349 snapshot
1350 );
1351 } else {
1352 this.buildSnapshot = snapshot;
1353 }
1354
1355 resolve();
1356 }
1357 );
1358 }
1359 );
1360 }
1361 );
1362 });
1363 } else {
1364 promise = Promise.resolve();
1365 }
1366 return promise.then(() => {
1367 if (reportProgress) reportProgress(0.8, "serialize pack");
1368 this.logger.time(`store pack`);
1369 const updatedBuildDependencies = new Set(this.buildDependencies);
1370 for (const dep of newBuildDependencies) {
1371 updatedBuildDependencies.add(dep);
1372 }
1373 const content = new PackContainer(
1374 pack,
1375 this.version,
1376 this.buildSnapshot,
1377 updatedBuildDependencies,
1378 this.resolveResults,
1379 this.resolveBuildDependenciesSnapshot
1380 );
1381 return this.fileSerializer
1382 .serialize(content, {
1383 filename: `${this.cacheLocation}/index${this._extension}`,
1384 extension: `${this._extension}`,
1385 logger: this.logger,
1386 profile: this.profile
1387 })
1388 .then(() => {
1389 for (const dep of newBuildDependencies) {
1390 this.buildDependencies.add(dep);
1391 }
1392 this.newBuildDependencies.clear();
1393 this.logger.timeEnd(`store pack`);
1394 const stats = pack.getContentStats();
1395 this.logger.log(
1396 "Stored pack (%d items, %d files, %d MiB)",
1397 pack.itemInfo.size,
1398 stats.count,
1399 Math.round(stats.size / 1024 / 1024)
1400 );
1401 })
1402 .catch(err => {
1403 this.logger.timeEnd(`store pack`);
1404 this.logger.warn(`Caching failed for pack: ${err}`);
1405 this.logger.debug(err.stack);
1406 });
1407 });
1408 })
1409 .catch(err => {
1410 this.logger.warn(`Caching failed for pack: ${err}`);
1411 this.logger.debug(err.stack);
1412 }));
1413 }
1414
1415 clear() {
1416 this.fileSystemInfo.clear();
1417 this.buildDependencies.clear();
1418 this.newBuildDependencies.clear();
1419 this.resolveBuildDependenciesSnapshot = undefined;
1420 this.resolveResults = undefined;
1421 this.buildSnapshot = undefined;
1422 this.packPromise = undefined;
1423 }
1424}
1425
1426module.exports = PackFileCacheStrategy;