Skip to content
Permalink
Browse files

perf(compiler): optimize cloning cursors state (#34332)

On a large compilation unit with big templates, the total time spent in
the `PlainCharacterCursor` constructor was 470ms. This commit applies
two optimizations to reduce this time:

1. Avoid the object spread operator within the constructor, as the
generated `__assign` helper in the emitted UMD bundle (ES5) does not
optimize well compared to a hardcoded object literal. This results in a
significant performance improvement. Because of the straight-forward
object literal, the VM is now much better able to optimize the memory
allocations which makes a significant difference as the
`PlainCharacterCursor` constructor is called in tight loops.

2. Reduce the number of `CharacterCursor` clones. Although cloning
itself is now much faster because of the optimization above, several
clone operations were not necessary.

Combined, these changes reduce the total time spent in the
`PlainCharacterCursor` constructor to just 10ms.

PR Close #34332
  • Loading branch information
JoostK authored and kara committed Dec 8, 2019
1 parent 82442c5 commit 5d871b56c69ee83e431c0fcd5b3a6a82d39e73fd
Showing with 19 additions and 5 deletions.
  1. +19 −5 packages/compiler/src/ml_parser/lexer.ts
@@ -242,7 +242,7 @@ class _Tokenizer {
this._currentTokenType = type;
}

private _endToken(parts: string[], end = this._cursor.clone()): Token {
private _endToken(parts: string[], end?: CharacterCursor): Token {
if (this._currentTokenStart === null) {
throw new TokenError(
'Programming error - attempted to end a token when there was no start to the token',
@@ -350,8 +350,7 @@ class _Tokenizer {
private _requireCharCodeUntilFn(predicate: (code: number) => boolean, len: number) {
const start = this._cursor.clone();
this._attemptCharCodeUntilFn(predicate);
const end = this._cursor.clone();
if (end.diff(start) < len) {
if (this._cursor.diff(start) < len) {
throw this._createError(
_unexpectedCharacterErrorMsg(this._cursor.peek()), this._cursor.getSpan(start));
}
@@ -821,7 +820,18 @@ class PlainCharacterCursor implements CharacterCursor {
this.file = fileOrCursor.file;
this.input = fileOrCursor.input;
this.end = fileOrCursor.end;
this.state = {...fileOrCursor.state};

const state = fileOrCursor.state;
// Note: avoid using `{...fileOrCursor.state}` here as that has a severe performance penalty.
// In ES5 bundles the object spread operator is translated into the `__assign` helper, which
// is not optimized by VMs as efficiently as a raw object literal. Since this constructor is
// called in tight loops, this difference matters.
this.state = {
peek: state.peek,
offset: state.offset,
line: state.line,
column: state.column,
};
} else {
if (!range) {
throw new Error(
@@ -851,9 +861,13 @@ class PlainCharacterCursor implements CharacterCursor {

getSpan(start?: this, leadingTriviaCodePoints?: number[]): ParseSourceSpan {
start = start || this;
let cloned = false;
if (leadingTriviaCodePoints) {
start = start.clone() as this;
while (this.diff(start) > 0 && leadingTriviaCodePoints.indexOf(start.peek()) !== -1) {
if (!cloned) {
start = start.clone() as this;
cloned = true;
}
start.advance();
}
}

0 comments on commit 5d871b5

Please sign in to comment.
You can’t perform that action at this time.