Skip to content

Commit

Permalink
[PERF] BundleWriter: Improve performance (#534)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
matz3 authored Oct 22, 2020
1 parent a9dff85 commit 750b43e
Show file tree
Hide file tree
Showing 2 changed files with 233 additions and 5 deletions.
19 changes: 14 additions & 5 deletions lib/lbt/bundle/BundleWriter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@


const NL = "\n";
const ENDS_WITH_NEW_LINE = /(^|\r\n|\r|\n)[ \t]*$/;
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
Expand All @@ -20,11 +21,19 @@ class BundleWriter {
this.segments = [];
this.currentSegment = null;
this.currentSourceIndex = 0;
this.endsWithNewLine = true; // Initially we don't need a new line
}

write(...str) {
let writeBuf = "";
for ( let i = 0; i < str.length; i++ ) {
this.buf += str[i];
writeBuf += str[i];
}
if ( writeBuf.length >= 1 ) {
this.buf += writeBuf;
this.endsWithNewLine =
ENDS_WITH_NEW_LINE.test(writeBuf) ||
(this.endsWithNewLine && SPACES_OR_TABS_ONLY.test(writeBuf));
}
}

Expand All @@ -33,12 +42,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;
}
}

Expand Down Expand Up @@ -75,4 +85,3 @@ class BundleWriter {
}

module.exports = BundleWriter;

219 changes: 219 additions & 0 deletions test/lib/lbt/bundle/BundleWriter.js
Original file line number Diff line number Diff line change
@@ -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"
});
});

0 comments on commit 750b43e

Please sign in to comment.