Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PERF] BundleWriter: Improve performance #534

Merged
merged 5 commits into from
Oct 22, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
});
});