From 4945274080276521ca6ccd93957a5dee1c395f90 Mon Sep 17 00:00:00 2001 From: JoostK Date: Sun, 8 Dec 2019 14:57:07 +0100 Subject: [PATCH] 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 --- packages/compiler/src/ml_parser/lexer.ts | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/compiler/src/ml_parser/lexer.ts b/packages/compiler/src/ml_parser/lexer.ts index cff42e4763..af02426e53 100644 --- a/packages/compiler/src/ml_parser/lexer.ts +++ b/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(); } }