diff --git a/lib/CachedSource.js b/lib/CachedSource.js index 5fc2d4c..60f3ec1 100644 --- a/lib/CachedSource.js +++ b/lib/CachedSource.js @@ -8,6 +8,9 @@ const Source = require("./Source"); const streamChunksOfSourceMap = require("./helpers/streamChunksOfSourceMap"); const streamChunksOfRawSource = require("./helpers/streamChunksOfRawSource"); const streamAndGetSourceAndMap = require("./helpers/streamAndGetSourceAndMap"); +const { + isDualStringBufferCachingEnabled +} = require("./helpers/stringBufferUtils"); const mapToBufferedMap = map => { if (typeof map !== "object" || !map) return map; @@ -63,15 +66,12 @@ class CachedSource extends Source { bufferedMap: cacheEntry.bufferedMap }); } - // We don't want to cache strings - // So if we have a caches sources - // create a buffer from it and only store - // if it was a Buffer or string - if (this._cachedSource) { - this.buffer(); - } return { - buffer: this._cachedBuffer, + // We don't want to cache strings + // So if we have a caches sources + // create a buffer from it and only store + // if it was a Buffer or string + buffer: this._cachedSource ? this.buffer() : this._cachedBuffer, source: this._cachedSourceType !== undefined ? this._cachedSourceType @@ -112,19 +112,26 @@ class CachedSource extends Source { _getCachedSource() { if (this._cachedSource !== undefined) return this._cachedSource; if (this._cachedBuffer && this._cachedSourceType !== undefined) { - return (this._cachedSource = this._cachedSourceType + const value = this._cachedSourceType ? this._cachedBuffer.toString("utf-8") - : this._cachedBuffer); + : this._cachedBuffer; + if (isDualStringBufferCachingEnabled()) { + this._cachedSource = value; + } + return value; } } buffer() { if (this._cachedBuffer !== undefined) return this._cachedBuffer; if (this._cachedSource !== undefined) { - if (Buffer.isBuffer(this._cachedSource)) { - return (this._cachedBuffer = this._cachedSource); + const value = Buffer.isBuffer(this._cachedSource) + ? this._cachedSource + : Buffer.from(this._cachedSource, "utf-8"); + if (isDualStringBufferCachingEnabled()) { + this._cachedBuffer = value; } - return (this._cachedBuffer = Buffer.from(this._cachedSource, "utf-8")); + return value; } if (typeof this.original().buffer === "function") { return (this._cachedBuffer = this.original().buffer()); @@ -133,7 +140,11 @@ class CachedSource extends Source { if (Buffer.isBuffer(bufferOrString)) { return (this._cachedBuffer = bufferOrString); } - return (this._cachedBuffer = Buffer.from(bufferOrString, "utf-8")); + const value = Buffer.from(bufferOrString, "utf-8"); + if (isDualStringBufferCachingEnabled()) { + this._cachedBuffer = value; + } + return value; } size() { diff --git a/lib/OriginalSource.js b/lib/OriginalSource.js index 59da29a..84a729a 100644 --- a/lib/OriginalSource.js +++ b/lib/OriginalSource.js @@ -9,6 +9,9 @@ const splitIntoLines = require("./helpers/splitIntoLines"); const getGeneratedSourceInfo = require("./helpers/getGeneratedSourceInfo"); const Source = require("./Source"); const splitIntoPotentialTokens = require("./helpers/splitIntoPotentialTokens"); +const { + isDualStringBufferCachingEnabled +} = require("./helpers/stringBufferUtils"); class OriginalSource extends Source { constructor(value, name) { @@ -25,14 +28,22 @@ class OriginalSource extends Source { source() { if (this._value === undefined) { - this._value = this._valueAsBuffer.toString("utf-8"); + const value = 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; } @@ -123,11 +134,8 @@ class OriginalSource extends Source { } updateHash(hash) { - if (this._valueAsBuffer === undefined) { - this._valueAsBuffer = Buffer.from(this._value, "utf-8"); - } hash.update("OriginalSource"); - hash.update(this._valueAsBuffer); + hash.update(this.buffer()); hash.update(this._name || ""); } } diff --git a/lib/RawSource.js b/lib/RawSource.js index 098d317..d52d2c6 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 { @@ -16,9 +20,16 @@ class RawSource extends Source { throw new TypeError("argument 'value' must be either string or Buffer"); } this._valueIsBuffer = !convertToString && isBuffer; - this._value = convertToString && isBuffer ? undefined : value; + const internedString = + typeof value === "string" ? internString(value) : undefined; + this._value = + convertToString && isBuffer + ? undefined + : typeof value === "string" + ? internedString + : value; this._valueAsBuffer = isBuffer ? value : undefined; - this._valueAsString = isBuffer ? undefined : value; + this._valueAsString = isBuffer ? undefined : internedString; } isBuffer() { @@ -27,14 +38,22 @@ class RawSource extends Source { source() { if (this._value === undefined) { - this._value = this._valueAsBuffer.toString("utf-8"); + const value = this._valueAsBuffer.toString("utf-8"); + if (isDualStringBufferCachingEnabled()) { + this._value = internString(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 +70,16 @@ class RawSource extends Source { * @returns {void} */ streamChunks(options, onChunk, onSource, onName) { - if (this._value === undefined) { - this._value = Buffer.from(this._valueAsBuffer, "utf-8"); - } - if (this._valueAsString === undefined) { - this._valueAsString = - typeof this._value === "string" - ? this._value - : this._value.toString("utf-8"); + let strValue = this._valueAsString; + if (strValue === undefined) { + const value = this.source(); + strValue = typeof value === "string" ? value : value.toString("utf-8"); + if (isDualStringBufferCachingEnabled()) { + this._valueAsString = internString(strValue); + } } return streamChunksOfRawSource( - this._valueAsString, + strValue, onChunk, onSource, onName, @@ -70,11 +88,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/SourceMapSource.js b/lib/SourceMapSource.js index 6b661fd..1cb5494 100644 --- a/lib/SourceMapSource.js +++ b/lib/SourceMapSource.js @@ -8,6 +8,9 @@ const Source = require("./Source"); const streamChunksOfSourceMap = require("./helpers/streamChunksOfSourceMap"); const streamChunksOfCombinedSourceMap = require("./helpers/streamChunksOfCombinedSourceMap"); const { getMap, getSourceAndMap } = require("./helpers/getFromStreamChunks"); +const { + isDualStringBufferCachingEnabled +} = require("./helpers/stringBufferUtils"); class SourceMapSource extends Source { constructor( @@ -59,145 +62,168 @@ class SourceMapSource extends Source { this._removeOriginalSource = removeOriginalSource; } - _ensureValueBuffer() { + getArgsAsBuffers() { + return [ + this.buffer(), + this._name, + this._sourceMapBuffer(), + this._originalSourceBuffer(), + this._innerSourceMapBuffer(), + this._removeOriginalSource + ]; + } + + buffer() { if (this._valueAsBuffer === undefined) { - this._valueAsBuffer = Buffer.from(this._valueAsString, "utf-8"); + const value = Buffer.from(this._valueAsString, "utf-8"); + if (isDualStringBufferCachingEnabled()) { + this._valueAsBuffer = value; + } + return value; } + return this._valueAsBuffer; } - _ensureValueString() { + source() { if (this._valueAsString === undefined) { - this._valueAsString = this._valueAsBuffer.toString("utf-8"); + const value = this._valueAsBuffer.toString("utf-8"); + if (isDualStringBufferCachingEnabled()) { + this._valueAsString = value; + } + return value; } + return this._valueAsString; } - _ensureOriginalSourceBuffer() { + _originalSourceBuffer() { if (this._originalSourceAsBuffer === undefined && this._hasOriginalSource) { - this._originalSourceAsBuffer = Buffer.from( - this._originalSourceAsString, - "utf-8" - ); + const value = Buffer.from(this._originalSourceAsString, "utf-8"); + if (isDualStringBufferCachingEnabled()) { + this._originalSourceAsBuffer = value; + } + return value; } + return this._originalSourceAsBuffer; } - _ensureOriginalSourceString() { + _originalSourceString() { if (this._originalSourceAsString === undefined && this._hasOriginalSource) { - this._originalSourceAsString = - this._originalSourceAsBuffer.toString("utf-8"); + const value = this._originalSourceAsBuffer.toString("utf-8"); + if (isDualStringBufferCachingEnabled()) { + this._originalSourceAsString = value; + } + return value; } + return this._originalSourceAsString; } - _ensureInnerSourceMapObject() { + _innerSourceMapObject() { if (this._innerSourceMapAsObject === undefined && this._hasInnerSourceMap) { - this._ensureInnerSourceMapString(); - this._innerSourceMapAsObject = JSON.parse(this._innerSourceMapAsString); + const value = JSON.parse(this._innerSourceMapString()); + if (isDualStringBufferCachingEnabled()) { + this._innerSourceMapAsObject = value; + } + return value; } + return this._innerSourceMapAsObject; } - _ensureInnerSourceMapBuffer() { + _innerSourceMapBuffer() { if (this._innerSourceMapAsBuffer === undefined && this._hasInnerSourceMap) { - this._ensureInnerSourceMapString(); - this._innerSourceMapAsBuffer = Buffer.from( - this._innerSourceMapAsString, - "utf-8" - ); + const value = Buffer.from(this._innerSourceMapString(), "utf-8"); + if (isDualStringBufferCachingEnabled()) { + this._innerSourceMapAsBuffer = value; + } + return value; } + return this._innerSourceMapAsBuffer; } - _ensureInnerSourceMapString() { + _innerSourceMapString() { if (this._innerSourceMapAsString === undefined && this._hasInnerSourceMap) { if (this._innerSourceMapAsBuffer !== undefined) { - this._innerSourceMapAsString = - this._innerSourceMapAsBuffer.toString("utf-8"); + const value = this._innerSourceMapAsBuffer.toString("utf-8"); + if (isDualStringBufferCachingEnabled()) { + this._innerSourceMapAsString = value; + } + return value; } else { - this._innerSourceMapAsString = JSON.stringify( - this._innerSourceMapAsObject - ); + const value = JSON.stringify(this._innerSourceMapAsObject); + if (isDualStringBufferCachingEnabled()) { + this._innerSourceMapAsString = value; + } + return value; } } + return this._innerSourceMapAsString; } - _ensureSourceMapObject() { + _sourceMapObject() { if (this._sourceMapAsObject === undefined) { - this._ensureSourceMapString(); - this._sourceMapAsObject = JSON.parse(this._sourceMapAsString); + const value = JSON.parse(this._sourceMapString()); + if (isDualStringBufferCachingEnabled()) { + this._sourceMapAsObject = value; + } + return value; } + return this._sourceMapAsObject; } - _ensureSourceMapBuffer() { + _sourceMapBuffer() { if (this._sourceMapAsBuffer === undefined) { - this._ensureSourceMapString(); - this._sourceMapAsBuffer = Buffer.from(this._sourceMapAsString, "utf-8"); + const value = Buffer.from(this._sourceMapString(), "utf-8"); + if (isDualStringBufferCachingEnabled()) { + this._sourceMapAsBuffer = value; + } + return value; } + return this._sourceMapAsBuffer; } - _ensureSourceMapString() { + _sourceMapString() { if (this._sourceMapAsString === undefined) { if (this._sourceMapAsBuffer !== undefined) { - this._sourceMapAsString = this._sourceMapAsBuffer.toString("utf-8"); + const value = this._sourceMapAsBuffer.toString("utf-8"); + if (isDualStringBufferCachingEnabled()) { + this._sourceMapAsString = value; + } + return value; } else { - this._sourceMapAsString = JSON.stringify(this._sourceMapAsObject); + const value = JSON.stringify(this._sourceMapAsObject); + if (isDualStringBufferCachingEnabled()) { + this._sourceMapAsString = value; + } + return value; } } - } - - getArgsAsBuffers() { - this._ensureValueBuffer(); - this._ensureSourceMapBuffer(); - this._ensureOriginalSourceBuffer(); - this._ensureInnerSourceMapBuffer(); - return [ - this._valueAsBuffer, - this._name, - this._sourceMapAsBuffer, - this._originalSourceAsBuffer, - this._innerSourceMapAsBuffer, - this._removeOriginalSource - ]; - } - - buffer() { - this._ensureValueBuffer(); - return this._valueAsBuffer; - } - - source() { - this._ensureValueString(); - return this._valueAsString; + return this._sourceMapAsString; } map(options) { if (!this._hasInnerSourceMap) { - this._ensureSourceMapObject(); - return this._sourceMapAsObject; + return this._sourceMapObject(); } return getMap(this, options); } sourceAndMap(options) { if (!this._hasInnerSourceMap) { - this._ensureValueString(); - this._ensureSourceMapObject(); return { - source: this._valueAsString, - map: this._sourceMapAsObject + source: this.source(), + map: this._sourceMapObject() }; } return getSourceAndMap(this, options); } streamChunks(options, onChunk, onSource, onName) { - this._ensureValueString(); - this._ensureSourceMapObject(); - this._ensureOriginalSourceString(); if (this._hasInnerSourceMap) { - this._ensureInnerSourceMapObject(); return streamChunksOfCombinedSourceMap( - this._valueAsString, - this._sourceMapAsObject, + this.source(), + this._sourceMapObject(), this._name, - this._originalSourceAsString, - this._innerSourceMapAsObject, + this._originalSourceString(), + this._innerSourceMapObject(), this._removeOriginalSource, onChunk, onSource, @@ -207,8 +233,8 @@ class SourceMapSource extends Source { ); } else { return streamChunksOfSourceMap( - this._valueAsString, - this._sourceMapAsObject, + this.source(), + this._sourceMapObject(), onChunk, onSource, onName, @@ -219,23 +245,16 @@ class SourceMapSource extends Source { } updateHash(hash) { - this._ensureValueBuffer(); - this._ensureSourceMapBuffer(); - this._ensureOriginalSourceBuffer(); - this._ensureInnerSourceMapBuffer(); - hash.update("SourceMapSource"); - - hash.update(this._valueAsBuffer); - - hash.update(this._sourceMapAsBuffer); + hash.update(this.buffer()); + hash.update(this._sourceMapBuffer()); if (this._hasOriginalSource) { - hash.update(this._originalSourceAsBuffer); + hash.update(this._originalSourceBuffer()); } if (this._hasInnerSourceMap) { - hash.update(this._innerSourceMapAsBuffer); + hash.update(this._innerSourceMapBuffer()); } hash.update(this._removeOriginalSource ? "true" : "false"); diff --git a/lib/helpers/stringBufferUtils.js b/lib/helpers/stringBufferUtils.js new file mode 100644 index 0000000..3ca9d48 --- /dev/null +++ b/lib/helpers/stringBufferUtils.js @@ -0,0 +1,120 @@ +/* + 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 || + str.length < 128 || + 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; +} + +/** + * Starts 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. + * + * {@link exitStringInterningRange} should be called when string interning is + * no longer necessary to free up the memory used by the interned strings. If + * {@link enterStringInterningRange} has been called multiple times, then + * this method may not immediately free all the memory until + * {@link exitStringInterningRange} has been called to end all string + * interning ranges. + * + * @returns {void} + */ +function enterStringInterningRange() { + enableStringInterningRefCount++; +} + +/** + * Stops the current string interning range. Once all string interning ranges + * have been exited, this method will free all the memory used by the interned + * strings. This method should be called once for each time that + * {@link enterStringInterningRange} was called. + * + * @returns {void} + */ +function exitStringInterningRange() { + if (--enableStringInterningRefCount <= 0) { + interningStringMap.clear(); + enableStringInterningRefCount = 0; + } +} + +module.exports = { + disableDualStringBufferCaching, + enableDualStringBufferCaching, + internString, + isDualStringBufferCachingEnabled, + enterStringInterningRange, + exitStringInterningRange +}; 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")); diff --git a/test/CachedSource.js b/test/CachedSource.js index e365f72..b63ad56 100644 --- a/test/CachedSource.js +++ b/test/CachedSource.js @@ -5,6 +5,12 @@ const OriginalSource = require("../").OriginalSource; const RawSource = require("../").RawSource; const Source = require("../").Source; const streamChunks = require("../lib/helpers/streamChunks"); +const { + enableDualStringBufferCaching, + enterStringInterningRange, + exitStringInterningRange, + disableDualStringBufferCaching +} = require("../lib/helpers/stringBufferUtils"); class TrackedSource extends Source { constructor(source) { @@ -66,7 +72,28 @@ const getHash = source => { return hash.digest("hex"); }; -describe("CachedSource", () => { +describe.each([ + { + enableMemoryOptimizations: false + }, + { + enableMemoryOptimizations: true + } +])("CachedSource %s", ({ enableMemoryOptimizations }) => { + beforeEach(() => { + if (enableMemoryOptimizations) { + disableDualStringBufferCaching(); + enterStringInterningRange(); + } + }); + + afterEach(() => { + if (enableMemoryOptimizations) { + enableDualStringBufferCaching(); + exitStringInterningRange(); + } + }); + it("should return the correct size for binary files", () => { const source = new OriginalSource(Buffer.from(new Array(256)), "file.wasm"); const cachedSource = new CachedSource(source); @@ -229,7 +256,7 @@ describe("CachedSource", () => { hash: 0 }); }); - it("should use an old webpack-sources Source", () => { + it("should use an old webpack-sources Source with Buffer", () => { const buffer = Buffer.from(new Array(256)); const source = new TrackedSource(new RawSource(buffer)); source.buffer = undefined; @@ -248,7 +275,7 @@ describe("CachedSource", () => { hash: 0 }); }); - it("should use an old webpack-sources Source", () => { + it("should use an old webpack-sources Source with String", () => { const string = "Hello World"; const source = new TrackedSource(new RawSource(string)); source.buffer = undefined; @@ -258,7 +285,11 @@ describe("CachedSource", () => { expect(Buffer.isBuffer(buffer)).toBe(true); expect(buffer.toString("utf-8")).toBe(string); - expect(cachedSource.buffer()).toBe(buffer); + if (enableMemoryOptimizations) { + expect(cachedSource.buffer().equals(buffer)).toBe(true); + } else { + expect(cachedSource.buffer()).toBe(buffer); + } expect(cachedSource.source()).toBe(string); expect(cachedSource.source()).toBe(string); expect(source.getCalls()).toEqual({ diff --git a/test/OriginalSource.js b/test/OriginalSource.js index a15829b..b469a80 100644 --- a/test/OriginalSource.js +++ b/test/OriginalSource.js @@ -1,7 +1,34 @@ jest.mock("../lib/helpers/createMappingsSerializer"); +const { + enableDualStringBufferCaching, + enterStringInterningRange, + exitStringInterningRange, + disableDualStringBufferCaching +} = require("../lib/helpers/stringBufferUtils"); const OriginalSource = require("../").OriginalSource; -describe("OriginalSource", () => { +describe.each([ + { + enableMemoryOptimizations: false + }, + { + enableMemoryOptimizations: true + } +])("OriginalSource %s", ({ enableMemoryOptimizations }) => { + beforeEach(() => { + if (enableMemoryOptimizations) { + disableDualStringBufferCaching(); + enterStringInterningRange(); + } + }); + + afterEach(() => { + if (enableMemoryOptimizations) { + enableDualStringBufferCaching(); + exitStringInterningRange(); + } + }); + it("should handle multiline string", () => { const source = new OriginalSource("Line1\n\nLine3\n", "file.js"); const resultText = source.source(); diff --git a/test/RawSource.js b/test/RawSource.js new file mode 100644 index 0000000..aeb04a5 --- /dev/null +++ b/test/RawSource.js @@ -0,0 +1,66 @@ +const RawSource = require("../").RawSource; +const { + enableDualStringBufferCaching, + enterStringInterningRange, + exitStringInterningRange, + disableDualStringBufferCaching +} = require("../lib/helpers/stringBufferUtils"); + +const CODE_STRING = + "console.log('test');\nconsole.log('test2');\nconsole.log('test22');\n"; + +describe("RawSource", () => { + it("converts to buffer correctly", () => { + const source = new RawSource(Buffer.from(CODE_STRING), true); + expect(source.isBuffer()).toEqual(false); + expect(source.buffer().toString("utf-8")).toEqual(CODE_STRING); + // The buffer conversion should be cached. + expect(source.buffer() === source.buffer()).toEqual(true); + }); + + it("stream chunks works correctly", () => { + const source = new RawSource(CODE_STRING, true); + source.streamChunks(null, (line, lineNum) => { + expect(line).toEqual(`console.log('test${"2".repeat(lineNum - 1)}');\n`); + }); + expect.assertions(3); + }); + + describe("memory optimizations are enabled", () => { + beforeEach(() => { + disableDualStringBufferCaching(); + enterStringInterningRange(); + }); + + afterEach(() => { + enableDualStringBufferCaching(); + exitStringInterningRange(); + }); + + it("should create new buffers when caching is not enabled", () => { + const source = new RawSource(CODE_STRING, true); + expect(source.buffer().toString("utf-8")).toEqual(CODE_STRING); + // The buffer conversion should not be cached. + expect(source.buffer() === source.buffer()).toEqual(false); + }); + + it("should not create new buffers when original value is a buffer", () => { + const originalValue = Buffer.from(CODE_STRING); + const source = new RawSource(originalValue, true); + expect(source.buffer().toString("utf-8")).toEqual(CODE_STRING); + // The same buffer as the original value should always be returned. + expect(originalValue === source.buffer()).toEqual(true); + expect(source.buffer() === source.buffer()).toEqual(true); + }); + + it("stream chunks works correctly", () => { + const source = new RawSource(CODE_STRING, true); + source.streamChunks(null, (line, lineNum) => { + expect(line).toEqual( + `console.log('test${"2".repeat(lineNum - 1)}');\n` + ); + }); + expect.assertions(3); + }); + }); +}); diff --git a/test/SourceMapSource.js b/test/SourceMapSource.js index 875c6b8..6c61d03 100644 --- a/test/SourceMapSource.js +++ b/test/SourceMapSource.js @@ -10,8 +10,35 @@ const SourceNode = require("source-map").SourceNode; const fs = require("fs"); const path = require("path"); const { withReadableMappings } = require("./helpers"); +const { + enableDualStringBufferCaching, + enterStringInterningRange, + exitStringInterningRange, + disableDualStringBufferCaching +} = require("../lib/helpers/stringBufferUtils"); + +describe.each([ + { + enableMemoryOptimizations: false + }, + { + enableMemoryOptimizations: true + } +])("SourceMapSource %s", ({ enableMemoryOptimizations }) => { + beforeEach(() => { + if (enableMemoryOptimizations) { + disableDualStringBufferCaching(); + enterStringInterningRange(); + } + }); + + afterEach(() => { + if (enableMemoryOptimizations) { + enableDualStringBufferCaching(); + exitStringInterningRange(); + } + }); -describe("SourceMapSource", () => { it("map correctly", () => { const innerSourceCode = ["Hello World", "is a test string"].join("\n") + "\n"; @@ -434,7 +461,12 @@ describe("SourceMapSource", () => { expect(buffer1.length).toBe(6); const buffer2 = sourceMapSource.buffer(); - expect(buffer2).toBe(buffer1); + if (enableMemoryOptimizations) { + // When memory optimizations are enabled, the buffer is not cached. + expect(buffer1.equals(buffer2)).toBe(true); + } else { + expect(buffer2).toBe(buffer1); + } }); it("provides buffer when backed by buffer", () => {