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
This commit is contained in:
JoostK 2019-12-08 14:57:07 +01:00 committed by Kara Erickson
parent 8c2cbdd385
commit 4945274080
1 changed files with 19 additions and 5 deletions

View File

@ -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();
}
}