webpack
Version:
Packs ECMAScript/CommonJs/AMD modules for the browser. Allows you to split your codebase into multiple bundles, which can be loaded on demand. Supports loaders to preprocess files, i.e. json, jsx, es7, css, less, ... and your custom stuff.
157 lines (147 loc) • 4.96 kB
JavaScript
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
;
const Cache = require("./Cache");
/** @import { Data, Etag } from "./Cache" */
/** @import Compiler from "../Compiler" */
/**
* Defines the memory with gc cache plugin options type used by this module.
* @typedef {object} MemoryWithGcCachePluginOptions
* @property {number} maxGenerations max generations
*/
const PLUGIN_NAME = "MemoryWithGcCachePlugin";
class MemoryWithGcCachePlugin {
/**
* Creates an instance of MemoryWithGcCachePlugin.
* @param {MemoryWithGcCachePluginOptions} options options
*/
constructor({ maxGenerations }) {
/** @type {number} */
this._maxGenerations = maxGenerations;
}
/**
* Applies the plugin by registering its hooks on the compiler.
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
const maxGenerations = this._maxGenerations;
/** @type {Map<string, { etag: Etag | null, data: Data } | undefined | null>} */
const cache = new Map();
/** @type {Map<string, { entry: { etag: Etag | null, data: Data } | null, until: number }>} */
const oldCache = new Map();
let generation = 0;
let cachePosition = 0;
const logger = compiler.getInfrastructureLogger(PLUGIN_NAME);
compiler.hooks.afterDone.tap(PLUGIN_NAME, () => {
generation++;
let clearedEntries = 0;
/** @type {undefined | string} */
let lastClearedIdentifier;
// Avoid coverage problems due indirect changes
/* istanbul ignore next */
for (const [identifier, entry] of oldCache) {
if (entry.until > generation) break;
oldCache.delete(identifier);
if (cache.get(identifier) === undefined) {
cache.delete(identifier);
clearedEntries++;
lastClearedIdentifier = identifier;
}
}
if (clearedEntries > 0 || oldCache.size > 0) {
logger.log(
`${cache.size - oldCache.size} active entries, ${
oldCache.size
} recently unused cached entries${
clearedEntries > 0
? `, ${clearedEntries} old unused cache entries removed e. g. ${lastClearedIdentifier}`
: ""
}`
);
}
let i = (cache.size / maxGenerations) | 0;
let j = cachePosition >= cache.size ? 0 : cachePosition;
cachePosition = j + i;
for (const [identifier, entry] of cache) {
if (j !== 0) {
j--;
continue;
}
if (entry !== undefined) {
// Set to undefined rather than deleted, reserving the slot and sparing
// a rehash as entries come and go. It goes for good once `oldCache`
// drops it.
cache.set(identifier, undefined);
oldCache.delete(identifier);
oldCache.set(identifier, {
entry,
until: generation + maxGenerations
});
if (i-- === 0) break;
}
}
});
compiler.cache.hooks.store.tap(
{ name: PLUGIN_NAME, stage: Cache.STAGE_MEMORY },
(identifier, etag, data) => {
cache.set(identifier, { etag, data });
}
);
compiler.cache.hooks.get.tap(
{ name: PLUGIN_NAME, stage: Cache.STAGE_MEMORY },
(identifier, etag, gotHandlers) => {
const cacheEntry = cache.get(identifier);
// A recorded miss: the whole chain was asked for this identifier already.
if (cacheEntry === null) return null;
// WHY: etags are compared by identity, and a lazy one is interned per
// source object, so equal content reached through a second object is a
// different etag. Hashing to tell those apart would cost every hit what
// laziness saves, so a mismatch falls through to the next stage instead:
// the file cache compares etags by value and can still answer. Returning
// `null` here would bail the hook and lose that, and `Cache.get` maps it
// to `undefined` regardless, so it buys the caller nothing.
let known = cacheEntry !== undefined;
if (cacheEntry !== undefined) {
if (cacheEntry.etag === etag) return cacheEntry.data;
} else {
const oldCacheEntry = oldCache.get(identifier);
if (oldCacheEntry !== undefined) {
const entry = oldCacheEntry.entry;
if (entry === null) {
oldCache.delete(identifier);
cache.set(identifier, entry);
return null;
}
known = true;
if (entry.etag === etag) {
oldCache.delete(identifier);
cache.set(identifier, entry);
return entry.data;
}
}
}
gotHandlers.push((result, callback) => {
if (result !== undefined) {
cache.set(identifier, { etag, data: result });
} else if (!known) {
// Record the miss only for an identifier nothing was known about:
// an entry reached with a different etag still answers its own.
cache.set(identifier, null);
}
return callback();
});
}
);
compiler.cache.hooks.shutdown.tap(
{ name: PLUGIN_NAME, stage: Cache.STAGE_MEMORY },
() => {
cache.clear();
oldCache.clear();
}
);
}
}
module.exports = MemoryWithGcCachePlugin;