From 713acb3677ba31ea9ad88f5a2f97c58f48df72d3 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Wed, 21 Oct 2020 12:34:00 +0200 Subject: [PATCH 1/5] [FIX] BundleWriter: Improve performance The ENDS_WITH_NEW_LINE RegExp is quite slow on large strings. Therefore the "endsWithNewLine" state is computed after every modification to the "buf" in order to improve performance. --- lib/lbt/bundle/BundleWriter.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/lbt/bundle/BundleWriter.js b/lib/lbt/bundle/BundleWriter.js index 2862fb35a..e3b761f27 100644 --- a/lib/lbt/bundle/BundleWriter.js +++ b/lib/lbt/bundle/BundleWriter.js @@ -20,12 +20,16 @@ class BundleWriter { this.segments = []; this.currentSegment = null; this.currentSourceIndex = 0; + this.endsWithNewLine = true; // Empty string matches ENDS_WITH_NEW_LINE pattern } write(...str) { + let writeBuf = ""; for ( let i = 0; i < str.length; i++ ) { - this.buf += str[i]; + writeBuf += str[i]; } + this.buf += writeBuf; + this.endsWithNewLine = ENDS_WITH_NEW_LINE.test(writeBuf); } writeln(...str) { @@ -33,12 +37,13 @@ class BundleWriter { this.buf += str[i]; } this.buf += NL; + this.endsWithNewLine = true; } ensureNewLine() { - // TODO this regexp might be quite expensive (use of $ anchor on long strings) - if ( !ENDS_WITH_NEW_LINE.test(this.buf) ) { + if ( !this.endsWithNewLine ) { this.buf += NL; + this.endsWithNewLine = true; } } From b6e960a4bcb92f77c2b41bca13d0d3420ac17ddf Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Wed, 21 Oct 2020 14:46:10 +0200 Subject: [PATCH 2/5] Fix tests --- lib/lbt/bundle/BundleWriter.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/lbt/bundle/BundleWriter.js b/lib/lbt/bundle/BundleWriter.js index e3b761f27..bd9288d74 100644 --- a/lib/lbt/bundle/BundleWriter.js +++ b/lib/lbt/bundle/BundleWriter.js @@ -28,8 +28,10 @@ class BundleWriter { for ( let i = 0; i < str.length; i++ ) { writeBuf += str[i]; } - this.buf += writeBuf; - this.endsWithNewLine = ENDS_WITH_NEW_LINE.test(writeBuf); + if ( writeBuf.length >= 1 ) { + this.buf += writeBuf; + this.endsWithNewLine = ENDS_WITH_NEW_LINE.test(writeBuf); + } } writeln(...str) { From 762c67cdd02ea1c2f1129a8cc8844a503fc83008 Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Wed, 21 Oct 2020 14:52:53 +0200 Subject: [PATCH 3/5] Fix regexp --- lib/lbt/bundle/BundleWriter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/lbt/bundle/BundleWriter.js b/lib/lbt/bundle/BundleWriter.js index bd9288d74..f627e4a1d 100644 --- a/lib/lbt/bundle/BundleWriter.js +++ b/lib/lbt/bundle/BundleWriter.js @@ -2,7 +2,7 @@ const NL = "\n"; -const ENDS_WITH_NEW_LINE = /(^|\r\n|\r|\n)[ \t]*$/; +const ENDS_WITH_NEW_LINE = /(\r\n|\r|\n)[ \t]*$/; /** * A filtering writer that can count written chars and provides some convenience @@ -20,7 +20,7 @@ class BundleWriter { this.segments = []; this.currentSegment = null; this.currentSourceIndex = 0; - this.endsWithNewLine = true; // Empty string matches ENDS_WITH_NEW_LINE pattern + this.endsWithNewLine = true; // Initially we don't need a new line } write(...str) { From 0e912580f9f89ba8d240373d32f489acf3a6b36c Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Thu, 22 Oct 2020 12:48:23 +0200 Subject: [PATCH 4/5] Add tests / fix endsWithNewLine detection --- lib/lbt/bundle/BundleWriter.js | 5 +- test/lib/lbt/bundle/BundleWriter.js | 219 ++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+), 1 deletion(-) create mode 100644 test/lib/lbt/bundle/BundleWriter.js diff --git a/lib/lbt/bundle/BundleWriter.js b/lib/lbt/bundle/BundleWriter.js index f627e4a1d..55101809f 100644 --- a/lib/lbt/bundle/BundleWriter.js +++ b/lib/lbt/bundle/BundleWriter.js @@ -3,6 +3,7 @@ const NL = "\n"; const ENDS_WITH_NEW_LINE = /(\r\n|\r|\n)[ \t]*$/; +const SPACES_OR_TABS_ONLY = /^[ \t]*$/; /** * A filtering writer that can count written chars and provides some convenience @@ -30,7 +31,9 @@ class BundleWriter { } if ( writeBuf.length >= 1 ) { this.buf += writeBuf; - this.endsWithNewLine = ENDS_WITH_NEW_LINE.test(writeBuf); + this.endsWithNewLine = + ENDS_WITH_NEW_LINE.test(writeBuf) || + (this.endsWithNewLine && SPACES_OR_TABS_ONLY.test(writeBuf)); } } diff --git a/test/lib/lbt/bundle/BundleWriter.js b/test/lib/lbt/bundle/BundleWriter.js new file mode 100644 index 000000000..202453959 --- /dev/null +++ b/test/lib/lbt/bundle/BundleWriter.js @@ -0,0 +1,219 @@ +const test = require("ava"); + +const BundleWriter = require("../../../../lib/lbt/bundle/BundleWriter"); + +test("Constructor", (t) => { + const w = new BundleWriter(); + t.is(w.buf, "", "Buffer should be an empty string"); + t.deepEqual(w.segments, [], "Segments should be empty"); + t.is(w.currentSegment, null, "No initial current segment"); + t.is(w.currentSourceIndex, 0, "Source index is initially at 0"); + t.is(w.endsWithNewLine, true, "Initially endsWithNewLine is true as buffer is empty"); +}); + +test("write", (t) => { + const w = new BundleWriter(); + t.is(w.toString(), "", "Output should be initially empty"); + w.write(""); + t.is(w.toString(), "", "Output should still be empty when writing an empty string"); + w.write("foo"); + t.is(w.toString(), "foo"); + w.write(" "); + t.is(w.toString(), "foo "); + w.write("bar"); + t.is(w.toString(), "foo bar"); + w.write(""); + t.is(w.toString(), "foo bar"); +}); + +test("write (endsWithNewLine)", (t) => { + const w = new BundleWriter(); + t.is(w.endsWithNewLine, true, "Initially endsWithNewLine is true as buffer is empty"); + + w.write(""); + t.is(w.endsWithNewLine, true, "endsWithNewLine should still be true after empty string"); + w.write(" "); + t.is(w.endsWithNewLine, true, "endsWithNewLine should still be true after writing spaces only"); + w.write("\t\t\t"); + t.is(w.endsWithNewLine, true, "endsWithNewLine should still be true after writing tabs only"); + w.write(" \t \t\t "); + t.is(w.endsWithNewLine, true, "endsWithNewLine should still be true after writing spaces and tabs only"); + + w.write("foo"); + t.is(w.endsWithNewLine, false, "endsWithNewLine should be false after writing 'foo'"); + w.write(" "); + t.is(w.endsWithNewLine, false, "endsWithNewLine should still be false after writing spaces only"); + w.write("\t\t\t"); + t.is(w.endsWithNewLine, false, "endsWithNewLine should still be false after writing tabs only"); + w.write(" \t \t\t "); + t.is(w.endsWithNewLine, false, "endsWithNewLine should still be false after writing spaces and tabs only"); + + w.write("foo\n"); + t.is(w.endsWithNewLine, true, "endsWithNewLine should be true after write with new-line"); + w.write(" "); + t.is(w.endsWithNewLine, true, "endsWithNewLine should still be true after writing spaces only"); + w.write("\t\t\t"); + t.is(w.endsWithNewLine, true, "endsWithNewLine should still be true after writing tabs only"); + w.write(" \t \t\t "); + t.is(w.endsWithNewLine, true, "endsWithNewLine should still be true after writing spaces and tabs only"); + + w.write("foo\nbar"); + t.is(w.endsWithNewLine, false, + "endsWithNewLine should be false after write that includes but not ends with new-line"); + + w.write("foo\n \t \t "); + t.is(w.endsWithNewLine, true, "endsWithNewLine should be true after write with new-line and tabs/spaces"); + w.write(" "); + t.is(w.endsWithNewLine, true, "endsWithNewLine should still be true after writing spaces only"); + w.write("\t\t\t"); + t.is(w.endsWithNewLine, true, "endsWithNewLine should still be true after writing tabs only"); + w.write(" \t \t\t "); + t.is(w.endsWithNewLine, true, "endsWithNewLine should still be true after writing spaces and tabs only"); +}); + +test("writeln", (t) => { + const w = new BundleWriter(); + t.is(w.toString(), "", "Output should be initially empty"); + w.writeln(""); + t.is(w.toString(), "\n", "Output should only contain a new-line"); + w.writeln("foo"); + t.is(w.toString(), "\nfoo\n"); + w.writeln(" "); + t.is(w.toString(), "\nfoo\n \n"); + w.writeln("bar"); + t.is(w.toString(), "\nfoo\n \nbar\n"); + w.writeln(""); + t.is(w.toString(), "\nfoo\n \nbar\n\n"); +}); + +test("writeln (endsWithNewLine)", (t) => { + const w = new BundleWriter(); + + w.endsWithNewLine = false; + + w.writeln(""); + t.is(w.endsWithNewLine, true, "endsWithNewLine should be true after writeln with empty string"); + + w.endsWithNewLine = false; + + w.writeln("c"); + t.is(w.endsWithNewLine, true, "endsWithNewLine should be true again after writeln with 'c'"); +}); + +test("ensureNewLine", (t) => { + const w = new BundleWriter(); + t.is(w.toString(), "", "Output should be initially empty"); + t.is(w.endsWithNewLine, true, "Initially endsWithNewLine is true as buffer is empty"); + + w.ensureNewLine(); + t.is(w.toString(), "", "Output should still be empty as no new-line is needed"); + + w.endsWithNewLine = false; + + w.ensureNewLine(); + t.is(w.toString(), "\n", "Output should contain a new-line as 'endsWithNewLine' was false"); + t.is(w.endsWithNewLine, true, "endsWithNewLine should be set to true"); +}); + +test("toString", (t) => { + const w = new BundleWriter(); + w.buf = "some string"; + t.is(w.toString(), "some string", "toString returns internal 'buf' property"); +}); + +test("length", (t) => { + const w = new BundleWriter(); + w.buf = "some string"; + t.is(w.length, "some string".length, "length returns internal 'buf' length"); +}); + +test("startSegment / endSegment", (t) => { + const w = new BundleWriter(); + + const module1 = {test: 1}; + + w.startSegment(module1); + + t.deepEqual(w.currentSegment, {module: {test: 1}, startIndex: 0}); + t.is(w.currentSegment.module, module1); + t.is(w.currentSourceIndex, 0); + t.deepEqual(w.segments, []); + + w.write("foo"); + + t.deepEqual(w.currentSegment, {module: {test: 1}, startIndex: 0}); + t.is(w.currentSegment.module, module1); + t.is(w.currentSourceIndex, 0); + t.deepEqual(w.segments, []); + + const targetSize1 = w.endSegment(); + + t.is(targetSize1, 3); + t.is(w.currentSegment, null); + t.is(w.currentSourceIndex, -1); + t.deepEqual(w.segments, [{ + module: {test: 1}, + startIndex: 0, + endIndex: 3 + }]); + + const module2 = {test: 2}; + + w.startSegment(module2); + + t.deepEqual(w.currentSegment, {module: {test: 2}, startIndex: 3}); + t.is(w.currentSegment.module, module2); + t.is(w.currentSourceIndex, 1); + t.deepEqual(w.segments, [{ + module: {test: 1}, + startIndex: 0, + endIndex: 3 + }]); + + w.write("bar!"); + + t.deepEqual(w.currentSegment, {module: {test: 2}, startIndex: 3}); + t.is(w.currentSegment.module, module2); + t.is(w.currentSourceIndex, 1); + t.deepEqual(w.segments, [{ + module: {test: 1}, + startIndex: 0, + endIndex: 3 + }]); + + const targetSize2 = w.endSegment(); + + t.is(targetSize2, 4); + t.is(w.currentSegment, null); + t.is(w.currentSourceIndex, -1); + t.deepEqual(w.segments, [{ + module: {test: 1}, + startIndex: 0, + endIndex: 3 + }, { + module: {test: 2}, + startIndex: 3, + endIndex: 7 + }]); +}); + +test("startSegment (Error handling)", (t) => { + const w = new BundleWriter(); + w.startSegment({}); + + t.throws(() => { + w.startSegment({}); + }, { + message: "trying to start a segment while another segment is still open" + }); +}); + +test("endSegment (Error handling)", (t) => { + const w = new BundleWriter(); + + t.throws(() => { + w.endSegment({}); + }, { + message: "trying to end a segment while no segment is open" + }); +}); From cc09738bde19479d5545e8ebaccd6279e6059f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matthias=20O=C3=9Fwald?= <1410947+matz3@users.noreply.github.com> Date: Thu, 22 Oct 2020 14:04:40 +0200 Subject: [PATCH 5/5] Update SPACES_OR_TABS_ONLY regexp Co-authored-by: Merlin Beutlberger --- lib/lbt/bundle/BundleWriter.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/lbt/bundle/BundleWriter.js b/lib/lbt/bundle/BundleWriter.js index 55101809f..8b63ba0ad 100644 --- a/lib/lbt/bundle/BundleWriter.js +++ b/lib/lbt/bundle/BundleWriter.js @@ -3,7 +3,7 @@ const NL = "\n"; const ENDS_WITH_NEW_LINE = /(\r\n|\r|\n)[ \t]*$/; -const SPACES_OR_TABS_ONLY = /^[ \t]*$/; +const SPACES_OR_TABS_ONLY = /^[ \t]+$/; /** * A filtering writer that can count written chars and provides some convenience @@ -85,4 +85,3 @@ class BundleWriter { } module.exports = BundleWriter; -