diff --git a/lib/RawSource.js b/lib/RawSource.js index 098d317..06e6b8f 100644 --- a/lib/RawSource.js +++ b/lib/RawSource.js @@ -6,6 +6,10 @@ "use strict"; const streamChunksOfRawSource = require("./helpers/streamChunksOfRawSource"); +const { + internString, + isDualStringBufferCachingEnabled +} = require("./helpers/stringBufferUtils"); const Source = require("./Source"); class RawSource extends Source { @@ -17,8 +21,13 @@ class RawSource extends Source { } this._valueIsBuffer = !convertToString && isBuffer; - this._value = convertToString && isBuffer ? undefined : value; + this._value = + convertToString && isBuffer + ? undefined + : typeof value === "string" + ? internString(value) + : value; this._valueAsBuffer = isBuffer ? value : undefined; - this._valueAsString = isBuffer ? undefined : value; + this._valueAsString = isBuffer ? undefined : internString(value); } isBuffer() { @@ -27,14 +36,22 @@ class RawSource extends Source { source() { if (this._value === undefined) { - this._value = this._valueAsBuffer.toString("utf-8"); + const value = internString(this._valueAsBuffer.toString("utf-8")); + if (isDualStringBufferCachingEnabled()) { + this._value = value; + } + return value; } return this._value; } buffer() { if (this._valueAsBuffer === undefined) { - this._valueAsBuffer = Buffer.from(this._value, "utf-8"); + const value = Buffer.from(this._value, "utf-8"); + if (isDualStringBufferCachingEnabled()) { + this._valueAsBuffer = value; + } + return value; } return this._valueAsBuffer; } @@ -51,17 +68,21 @@ class RawSource extends Source { * @returns {void} */ streamChunks(options, onChunk, onSource, onName) { - if (this._value === undefined) { + if (this._value === undefined && isDualStringBufferCachingEnabled()) { this._value = Buffer.from(this._valueAsBuffer, "utf-8"); } - if (this._valueAsString === undefined) { - this._valueAsString = + let strValue = this._valueAsString; + if (strValue === undefined) { + strValue = typeof this._value === "string" ? this._value - : this._value.toString("utf-8"); + : internString(this._value.toString("utf-8")); + if (isDualStringBufferCachingEnabled()) { + this._valueAsString = strValue; + } } return streamChunksOfRawSource( - this._valueAsString, + strValue, onChunk, onSource, onName, @@ -70,11 +91,8 @@ class RawSource extends Source { } updateHash(hash) { - if (this._valueAsBuffer === undefined) { - this._valueAsBuffer = Buffer.from(this._value, "utf-8"); - } hash.update("RawSource"); - hash.update(this._valueAsBuffer); + hash.update(this.buffer()); } } diff --git a/lib/helpers/stringBufferUtils.js b/lib/helpers/stringBufferUtils.js new file mode 100644 index 0000000..3b210f1 --- /dev/null +++ b/lib/helpers/stringBufferUtils.js @@ -0,0 +1,107 @@ +/* + MIT License http://www.opensource.org/licenses/mit-license.php + Author Mark Knichel @mknichel +*/ + +"use strict"; + +let dualStringBufferCaching = true; + +/** + * @returns {boolean} Whether the optimization to cache copies of both the + * string and buffer version of source content is enabled. This is enabled by + * default to improve performance but can consume more memory since values are + * stored twice. + */ +function isDualStringBufferCachingEnabled() { + return dualStringBufferCaching; +} + +/** + * Enables an optimization to save both string and buffer in memory to avoid + * repeat conversions between the two formats when they are requested. This + * is enabled by default. This option can improve performance but can consume + * additional memory since values are stored twice. + * + * @returns {void} + */ +function enableDualStringBufferCaching() { + dualStringBufferCaching = true; +} + +/** + * Disables the optimization to save both string and buffer in memory. This + * may increase performance but should reduce memory usage in the Webpack + * compiler. + * + * @returns {void} + */ +function disableDualStringBufferCaching() { + dualStringBufferCaching = false; +} + +const interningStringMap = new Map(); + +/** + * Saves the string in a map to ensure that only one copy of the string exists + * in memory at a given time. This is controlled by {@link enableStringInterning} + * and {@link disableStringInterning}. Callers are expect to manage the memory + * of the interned strings by calling {@link disableStringInterning} after the + * compiler no longer needs to save the interned memory. + * + * @param {string} str A string to be interned. + * @returns {string} The original string or a reference to an existing string + * of the same value if it has already been interned. + */ +function internString(str) { + if (!isStringInterningEnabled() || !str || typeof str !== "string") { + return str; + } + let internedString = interningStringMap.get(str); + if (internedString === undefined) { + internedString = str; + interningStringMap.set(str, internedString); + } + return internedString; +} + +let enableStringInterningRefCount = 0; + +function isStringInterningEnabled() { + return enableStringInterningRefCount > 0; +} + +/** + * Enables a memory optimization to avoid repeat copies of the same string in + * memory by caching a single reference to the string. This can reduce memory + * usage if the same string is repeated many times in the compiler, such as + * when Webpack layers are used with the same files. + * + * @returns {void} + */ +function enableStringInterning() { + enableStringInterningRefCount++; +} + +/** + * Disables string interning. This should be called to free the memory used by + * the interned strings after the compiler no longer needs to reuse the + * interned strings such as at the end of the compilation. + * + * @returns {void} + */ +function disableStringInterning() { + if (--enableStringInterningRefCount <= 0) { + interningStringMap.clear(); + enableStringInterningRefCount = 0; + } +} + +module.exports = { + disableDualStringBufferCaching, + disableStringInterning, + enableDualStringBufferCaching, + enableStringInterning, + internString, + isDualStringBufferCachingEnabled +}; diff --git a/lib/index.js b/lib/index.js index 0c11c2f..86a7234 100644 --- a/lib/index.js +++ b/lib/index.js @@ -28,3 +28,4 @@ defineExport("ReplaceSource", () => require("./ReplaceSource")); defineExport("PrefixSource", () => require("./PrefixSource")); defineExport("SizeOnlySource", () => require("./SizeOnlySource")); defineExport("CompatSource", () => require("./CompatSource")); +defineExport("stringBufferUtils", () => require("./helpers/stringBufferUtils"));