Skip to content

Commit

Permalink
Merge pull request #12990 from webpack/bugfix/memory-leak-in-ic
Browse files Browse the repository at this point in the history
memory usage improvements, add GC support for memory cache, persistent cache only mode
  • Loading branch information
sokra committed Apr 1, 2021
2 parents 0522deb + c84329f commit 3b8d26d
Show file tree
Hide file tree
Showing 17 changed files with 462 additions and 57 deletions.
12 changes: 12 additions & 0 deletions declarations/WebpackOptions.d.ts
Expand Up @@ -903,6 +903,10 @@ export interface WebpackOptions {
* Options object for in-memory caching.
*/
export interface MemoryCacheOptions {
/**
* Number of generations unused cache entries stay in memory cache at minimum (1 = may be removed after unused for a single compilation, ..., Infinity: kept forever).
*/
maxGenerations?: number;
/**
* In memory caching.
*/
Expand Down Expand Up @@ -949,6 +953,14 @@ export interface FileCacheOptions {
* List of paths that are managed by a package manager and can be trusted to not be modified otherwise.
*/
managedPaths?: string[];
/**
* Time for which unused cache entries stay in the filesystem cache at minimum (in milliseconds).
*/
maxAge?: number;
/**
* Number of generations unused cache entries stay in memory cache at minimum (0 = no memory cache used, 1 = may be removed after unused for a single compilation, ..., Infinity: kept forever). Cache entries will be deserialized from disk when removed from memory cache.
*/
maxMemoryGenerations?: number;
/**
* Name for the cache. Different names will lead to different coexisting caches.
*/
Expand Down
4 changes: 4 additions & 0 deletions lib/Compiler.js
Expand Up @@ -1101,6 +1101,10 @@ ${other}`);
close(callback) {
this.hooks.shutdown.callAsync(err => {
if (err) return callback(err);
// Get rid of reference to last compilation to avoid leaking memory
// We can't run this._cleanupLastCompilation() as the Stats to this compilation
// might be still in use. We try to get rid for the reference to the cache instead.
this._lastCompilation = undefined;
this.cache.shutdown(callback);
});
}
Expand Down
44 changes: 44 additions & 0 deletions lib/FileSystemInfo.js
Expand Up @@ -412,6 +412,14 @@ class SnapshotOptimization {
} times referenced)`;
}

clear() {
this._map.clear();
this._statItemsShared = 0;
this._statItemsUnshared = 0;
this._statSharedSnapshots = 0;
this._statReusedSharedSnapshots = 0;
}

storeUnsharedSnapshot(snapshot, locations) {
if (locations === undefined) return;
const optimizationEntry = {
Expand Down Expand Up @@ -984,6 +992,42 @@ class FileSystemInfo {
}
}

clear() {
this._remainingLogs = this.logger ? 40 : 0;
if (this._loggedPaths !== undefined) this._loggedPaths.clear();

this._snapshotCache = new WeakMap();
this._fileTimestampsOptimization.clear();
this._fileHashesOptimization.clear();
this._fileTshsOptimization.clear();
this._contextTimestampsOptimization.clear();
this._contextHashesOptimization.clear();
this._contextTshsOptimization.clear();
this._missingExistenceOptimization.clear();
this._managedItemInfoOptimization.clear();
this._managedFilesOptimization.clear();
this._managedContextsOptimization.clear();
this._managedMissingOptimization.clear();
this._fileTimestamps.clear();
this._fileHashes.clear();
this._fileTshs.clear();
this._contextTimestamps.clear();
this._contextHashes.clear();
this._contextTshs.clear();
this._managedItems.clear();
this._managedItems.clear();

this._cachedDeprecatedFileTimestamps = undefined;
this._cachedDeprecatedContextTimestamps = undefined;

this._statCreatedSnapshots = 0;
this._statTestedSnapshotsCached = 0;
this._statTestedSnapshotsNotCached = 0;
this._statTestedChildrenCached = 0;
this._statTestedChildrenNotCached = 0;
this._statTestedEntries = 0;
}

/**
* @param {Map<string, FileSystemInfoEntry | "ignore" | null>} map timestamps
* @returns {void}
Expand Down
3 changes: 3 additions & 0 deletions lib/NormalModule.js
Expand Up @@ -749,6 +749,9 @@ class NormalModule extends Module {
}
},
(err, result) => {
// Cleanup loaderContext to avoid leaking memory in ICs
loaderContext._compilation = loaderContext._compiler = loaderContext._module = loaderContext.fs = undefined;

if (!result) {
return processResult(
err || new Error("No result from loader-runner processing"),
Expand Down
31 changes: 24 additions & 7 deletions lib/WebpackOptionsApply.js
Expand Up @@ -521,9 +521,17 @@ class WebpackOptionsApply extends OptionsApply {
const cacheOptions = options.cache;
switch (cacheOptions.type) {
case "memory": {
//@ts-expect-error https://github.com/microsoft/TypeScript/issues/41697
const MemoryCachePlugin = require("./cache/MemoryCachePlugin");
new MemoryCachePlugin().apply(compiler);
if (isFinite(cacheOptions.maxGenerations)) {
//@ts-expect-error https://github.com/microsoft/TypeScript/issues/41697
const MemoryWithGcCachePlugin = require("./cache/MemoryWithGcCachePlugin");
new MemoryWithGcCachePlugin({
maxGenerations: cacheOptions.maxGenerations
}).apply(compiler);
} else {
//@ts-expect-error https://github.com/microsoft/TypeScript/issues/41697
const MemoryCachePlugin = require("./cache/MemoryCachePlugin");
new MemoryCachePlugin().apply(compiler);
}
break;
}
case "filesystem": {
Expand All @@ -532,9 +540,17 @@ class WebpackOptionsApply extends OptionsApply {
const list = cacheOptions.buildDependencies[key];
new AddBuildDependenciesPlugin(list).apply(compiler);
}
//@ts-expect-error https://github.com/microsoft/TypeScript/issues/41697
const MemoryCachePlugin = require("./cache/MemoryCachePlugin");
new MemoryCachePlugin().apply(compiler);
if (!isFinite(cacheOptions.maxMemoryGenerations)) {
//@ts-expect-error https://github.com/microsoft/TypeScript/issues/41697
const MemoryCachePlugin = require("./cache/MemoryCachePlugin");
new MemoryCachePlugin().apply(compiler);
} else if (cacheOptions.maxMemoryGenerations !== 0) {
//@ts-expect-error https://github.com/microsoft/TypeScript/issues/41697
const MemoryWithGcCachePlugin = require("./cache/MemoryWithGcCachePlugin");
new MemoryWithGcCachePlugin({
maxGenerations: cacheOptions.maxMemoryGenerations
}).apply(compiler);
}
switch (cacheOptions.store) {
case "pack": {
const IdleFileCachePlugin = require("./cache/IdleFileCachePlugin");
Expand All @@ -549,7 +565,8 @@ class WebpackOptionsApply extends OptionsApply {
logger: compiler.getInfrastructureLogger(
"webpack.cache.PackFileCacheStrategy"
),
snapshot: options.snapshot
snapshot: options.snapshot,
maxAge: cacheOptions.maxAge
}),
cacheOptions.idleTimeout,
cacheOptions.idleTimeoutForInitialStore
Expand Down
42 changes: 26 additions & 16 deletions lib/cache/IdleFileCachePlugin.js
Expand Up @@ -30,7 +30,7 @@ class IdleFileCachePlugin {
* @returns {void}
*/
apply(compiler) {
const strategy = this.strategy;
let strategy = this.strategy;
const idleTimeout = this.idleTimeout;
const idleTimeoutForInitialStore = Math.min(
idleTimeout,
Expand All @@ -54,20 +54,27 @@ class IdleFileCachePlugin {
compiler.cache.hooks.get.tapPromise(
{ name: "IdleFileCachePlugin", stage: Cache.STAGE_DISK },
(identifier, etag, gotHandlers) => {
return strategy.restore(identifier, etag).then(cacheEntry => {
if (cacheEntry === undefined) {
gotHandlers.push((result, callback) => {
if (result !== undefined) {
pendingIdleTasks.set(identifier, () =>
strategy.store(identifier, etag, result)
);
}
callback();
});
} else {
return cacheEntry;
}
});
const restore = () =>
strategy.restore(identifier, etag).then(cacheEntry => {
if (cacheEntry === undefined) {
gotHandlers.push((result, callback) => {
if (result !== undefined) {
pendingIdleTasks.set(identifier, () =>
strategy.store(identifier, etag, result)
);
}
callback();
});
} else {
return cacheEntry;
}
});
const pendingTask = pendingIdleTasks.get(identifier);
if (pendingTask !== undefined) {
pendingIdleTasks.delete(identifier);
return pendingTask().then(restore);
}
return restore();
}
);

Expand Down Expand Up @@ -101,7 +108,10 @@ class IdleFileCachePlugin {
reportProgress(1, `stored`);
});
}
return currentIdlePromise;
return currentIdlePromise.then(() => {
// Reset strategy
if (strategy.clear) strategy.clear();
});
}
);

Expand Down
6 changes: 6 additions & 0 deletions lib/cache/MemoryCachePlugin.js
Expand Up @@ -46,6 +46,12 @@ class MemoryCachePlugin {
});
}
);
compiler.cache.hooks.shutdown.tap(
{ name: "MemoryCachePlugin", stage: Cache.STAGE_MEMORY },
() => {
cache.clear();
}
);
}
}
module.exports = MemoryCachePlugin;
129 changes: 129 additions & 0 deletions lib/cache/MemoryWithGcCachePlugin.js
@@ -0,0 +1,129 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/

"use strict";

const Cache = require("../Cache");

/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("../Cache").Etag} Etag */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../Module")} Module */

class MemoryWithGcCachePlugin {
constructor({ maxGenerations }) {
this._maxGenerations = maxGenerations;
}
/**
* Apply the plugin
* @param {Compiler} compiler the compiler instance
* @returns {void}
*/
apply(compiler) {
const maxGenerations = this._maxGenerations;
/** @type {Map<string, { etag: Etag | null, data: any }>} */
const cache = new Map();
/** @type {Map<string, { entry: { etag: Etag | null, data: any }, until: number }>} */
const oldCache = new Map();
let generation = 0;
let cachePosition = 0;
const logger = compiler.getInfrastructureLogger("MemoryWithGcCachePlugin");
compiler.hooks.afterDone.tap("MemoryWithGcCachePlugin", () => {
generation++;
let clearedEntries = 0;
let lastClearedIdentifier;
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) {
// We don't delete the cache entry, but set it to undefined instead
// This reserves the location in the data table and avoids rehashing
// when constantly adding and removing entries.
// It will be deleted when removed from oldCache.
cache.set(identifier, undefined);
oldCache.delete(identifier);
oldCache.set(identifier, {
entry,
until: generation + maxGenerations
});
if (i-- === 0) break;
}
}
});
compiler.cache.hooks.store.tap(
{ name: "MemoryWithGcCachePlugin", stage: Cache.STAGE_MEMORY },
(identifier, etag, data) => {
cache.set(identifier, { etag, data });
}
);
compiler.cache.hooks.get.tap(
{ name: "MemoryWithGcCachePlugin", stage: Cache.STAGE_MEMORY },
(identifier, etag, gotHandlers) => {
const cacheEntry = cache.get(identifier);
if (cacheEntry === null) {
return null;
} else if (cacheEntry !== undefined) {
return cacheEntry.etag === etag ? cacheEntry.data : null;
}
const oldCacheEntry = oldCache.get(identifier);
if (oldCacheEntry !== undefined) {
const cacheEntry = oldCacheEntry.entry;
if (cacheEntry === null) {
oldCache.delete(identifier);
cache.set(identifier, cacheEntry);
return null;
} else {
if (cacheEntry.etag !== etag) return null;
oldCache.delete(identifier);
cache.set(identifier, cacheEntry);
return cacheEntry.data;
}
}
gotHandlers.push((result, callback) => {
if (result === undefined) {
cache.set(identifier, null);
} else {
cache.set(identifier, { etag, data: result });
}
return callback();
});
}
);
compiler.cache.hooks.shutdown.tap(
{ name: "MemoryWithGcCachePlugin", stage: Cache.STAGE_MEMORY },
() => {
cache.clear();
oldCache.clear();
}
);
}
}
module.exports = MemoryWithGcCachePlugin;

0 comments on commit 3b8d26d

Please sign in to comment.