compute asset hash lazily to avoid hash computation when memory cache is used

Cache accepts null as break condition when etag mismatches
MemoryCachePlugin caches cache mismatches
This commit is contained in:
Tobias Koppers 2019-11-12 13:55:13 +01:00
parent 1d9ae9d619
commit 1a3b391352
5 changed files with 111 additions and 20 deletions

View File

@ -13,6 +13,11 @@ const {
/** @typedef {import("./WebpackError")} WebpackError */
/**
* @typedef {Object} Etag
* @property {function(): string} toString
*/
/**
* @template T
* @callback Callback
@ -43,9 +48,9 @@ const needCalls = (times, callback) => {
class Cache {
constructor() {
this.hooks = {
/** @type {AsyncSeriesBailHook<[string, string, GotHandler[]], any>} */
/** @type {AsyncSeriesBailHook<[string, Etag | null, GotHandler[]], any>} */
get: new AsyncSeriesBailHook(["identifier", "etag", "gotHandlers"]),
/** @type {AsyncParallelHook<[string, string, any]>} */
/** @type {AsyncParallelHook<[string, Etag | null, any]>} */
store: new AsyncParallelHook(["identifier", "etag", "data"]),
/** @type {AsyncParallelHook<[Iterable<string>]>} */
storeBuildDependencies: new AsyncParallelHook(["dependencies"]),
@ -61,7 +66,7 @@ class Cache {
/**
* @template T
* @param {string} identifier the cache identifier
* @param {string} etag the etag
* @param {Etag | null} etag the etag
* @param {Callback<T>} callback signals when the value is retrieved
* @returns {void}
*/
@ -72,6 +77,9 @@ class Cache {
callback(makeWebpackError(err, "Cache.hooks.get"));
return;
}
if (result === null) {
result = undefined;
}
if (gotHandlers.length > 1) {
const innerCallback = needCalls(gotHandlers.length, () =>
callback(null, result)
@ -90,7 +98,7 @@ class Cache {
/**
* @template T
* @param {string} identifier the cache identifier
* @param {string} etag the etag
* @param {Etag | null} etag the etag
* @param {T} data the value to store
* @param {Callback<void>} callback signals when the value is stored
* @returns {void}

View File

@ -11,6 +11,7 @@ const { ConcatSource, RawSource } = require("webpack-sources");
const ModuleFilenameHelpers = require("./ModuleFilenameHelpers");
const ProgressPlugin = require("./ProgressPlugin");
const SourceMapDevToolModuleOptionsPlugin = require("./SourceMapDevToolModuleOptionsPlugin");
const getLazyHashedEtag = require("./cache/getLazyHashedEtag");
const createHash = require("./util/createHash");
const { relative, dirname } = require("./util/fs");
const { absolutify } = require("./util/identifier");
@ -20,6 +21,7 @@ const schema = require("../schemas/plugins/SourceMapDevToolPlugin.json");
/** @typedef {import("source-map").RawSourceMap} SourceMap */
/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").SourceMapDevToolPluginOptions} SourceMapDevToolPluginOptions */
/** @typedef {import("./Cache").Etag} Etag */
/** @typedef {import("./Chunk")} Chunk */
/** @typedef {import("./Compilation")} Compilation */
/** @typedef {import("./Compiler")} Compiler */
@ -34,7 +36,7 @@ const schema = require("../schemas/plugins/SourceMapDevToolPlugin.json");
* @property {string} file
* @property {SourceMap} sourceMap
* @property {string} cacheIdent cache identifier
* @property {string} cacheETag cache ETag
* @property {Etag} cacheETag cache ETag
*/
/**
@ -44,7 +46,7 @@ const schema = require("../schemas/plugins/SourceMapDevToolPlugin.json");
* @param {SourceMapDevToolPluginOptions} options source map options
* @param {Compilation} compilation compilation instance
* @param {string} cacheIdent cache identifier
* @param {string} cacheETag cache ETag
* @param {Etag} cacheETag cache ETag
* @returns {SourceMapTask | undefined} created task instance or `undefined`
*/
const getTaskForFile = (
@ -187,11 +189,7 @@ class SourceMapDevToolPlugin {
(file, callback) => {
const asset = compilation.getAsset(file).source;
const cacheIdent = `${compilation.compilerPath}/SourceMapDevToolPlugin/${file}`;
const cacheETagHash = createHash("md4");
asset.updateHash(cacheETagHash);
const cacheETag = /** @type {string} */ (cacheETagHash.digest(
"hex"
));
const cacheETag = getLazyHashedEtag(asset);
compilation.cache.get(cacheIdent, cacheETag, (err, assets) => {
if (err) {

View File

@ -8,6 +8,7 @@
const Cache = require("../Cache");
/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("../Cache").Etag} Etag */
/** @typedef {import("../Compiler")} Compiler */
/** @typedef {import("../Module")} Module */
@ -17,7 +18,7 @@ class MemoryCachePlugin {
* @returns {void}
*/
apply(compiler) {
/** @type {Map<string, { etag: string, data: any }>} */
/** @type {Map<string, { etag: Etag | null, data: any }>} */
const cache = new Map();
compiler.cache.hooks.store.tap(
{ name: "MemoryCachePlugin", stage: Cache.STAGE_MEMORY },
@ -29,11 +30,15 @@ class MemoryCachePlugin {
{ name: "MemoryCachePlugin", stage: Cache.STAGE_MEMORY },
(identifier, etag, gotHandlers) => {
const cacheEntry = cache.get(identifier);
if (cacheEntry !== undefined && cacheEntry.etag === etag) {
return cacheEntry.data;
if (cacheEntry === null) {
return null;
} else if (cacheEntry !== undefined) {
return cacheEntry.etag === etag ? cacheEntry.data : null;
}
gotHandlers.push((result, callback) => {
if (result !== undefined) {
if (result === undefined) {
cache.set(identifier, null);
} else {
cache.set(identifier, { etag, data: result });
}
return callback();

View File

@ -15,6 +15,7 @@ const {
MEASURE_END_OPERATION
} = require("../util/serialization");
/** @typedef {import("../Cache").Etag} Etag */
/** @typedef {import("../logging/Logger").Logger} Logger */
/** @typedef {import("../util/fs").IntermediateFileSystem} IntermediateFileSystem */
@ -72,6 +73,7 @@ makeSerializable(
class Pack {
constructor(logger) {
/** @type {Map<string, string | null>} */
this.etags = new Map();
/** @type {Map<string, any | (() => Promise<PackEntry>)>} */
this.content = new Map();
@ -83,10 +85,15 @@ class Pack {
this.logger = logger;
}
/**
* @param {string} identifier unique name for the resource
* @param {string | null} etag etag of the resource
* @returns {any} cached content
*/
get(identifier, etag) {
const etagInCache = this.etags.get(identifier);
if (etagInCache === undefined) return undefined;
if (etagInCache !== etag) return undefined;
if (etagInCache !== etag) return null;
this.used.add(identifier);
const content = this.content.get(identifier);
if (typeof content === "function") {
@ -98,6 +105,12 @@ class Pack {
}
}
/**
* @param {string} identifier unique name for the resource
* @param {string | null} etag etag of the resource
* @param {any} data cached content
* @returns {void}
*/
set(identifier, etag, data) {
if (this.unserializable.has(identifier)) return;
this.used.add(identifier);
@ -112,7 +125,7 @@ class Pack {
this.logger.debug(`Pack got invalid because of ${identifier}`);
}
this.etags.set(identifier, etag);
return this.content.set(identifier, data);
this.content.set(identifier, data);
}
collectGarbage(maxAge) {
@ -459,16 +472,29 @@ class PackFileCacheStrategy {
});
}
store(identifier, etag, data, idleTasks) {
/**
* @param {string} identifier unique name for the resource
* @param {Etag | null} etag etag of the resource
* @param {any} data cached content
* @returns {Promise<void>} promise
*/
store(identifier, etag, data) {
return this.packPromise.then(pack => {
this.logger.debug(`Cached ${identifier} to pack.`);
pack.set(identifier, etag, data);
pack.set(identifier, etag === null ? null : etag.toString(), data);
});
}
/**
* @param {string} identifier unique name for the resource
* @param {Etag | null} etag etag of the resource
* @returns {Promise<any>} promise to the cached content
*/
restore(identifier, etag) {
return this.packPromise
.then(pack => pack.get(identifier, etag))
.then(pack =>
pack.get(identifier, etag === null ? null : etag.toString())
)
.catch(err => {
if (err && err.code !== "ENOENT") {
this.logger.warn(

54
lib/cache/getLazyHashedEtag.js vendored Normal file
View File

@ -0,0 +1,54 @@
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const createHash = require("../util/createHash");
/** @typedef {import("../util/Hash")} Hash */
/**
* @typedef {Object} HashableObject
* @property {function(Hash): void} updateHash
*/
class LazyHashedEtag {
/**
* @param {HashableObject} obj object with updateHash method
*/
constructor(obj) {
this._obj = obj;
this._hash = undefined;
}
/**
* @returns {string} hash of object
*/
toString() {
if (this._hash === undefined) {
const hash = createHash("md4");
this._obj.updateHash(hash);
this._hash = /** @type {string} */ (hash.digest("base64"));
}
return this._hash;
}
}
/** @type {WeakMap<HashableObject, LazyHashedEtag>} */
const map = new WeakMap();
/**
* @param {HashableObject} obj object with updateHash method
* @returns {LazyHashedEtag} etag
*/
const getter = obj => {
const hash = map.get(obj);
if (hash !== undefined) return hash;
const newHash = new LazyHashedEtag(obj);
map.set(obj, newHash);
return newHash;
};
module.exports = getter;