Skip to content

chore(perf): improve performance of iterator usage #4084

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

Closed
Closed
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
3 changes: 3 additions & 0 deletions resources/build-npm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
showDirStats,
writeGeneratedFile,
} from './utils.js';
import { optimizeArrayDestructuring } from './optimize-array-destructuring.js';
import { optimizeForOf } from './optimize-for-of.js'

console.log('\n./npmDist');
buildPackage('./npmDist', false);
Expand Down Expand Up @@ -139,6 +141,7 @@ function emitTSFiles(options: {

const tsProgram = ts.createProgram(['src/index.ts'], tsOptions, tsHost);
const tsResult = tsProgram.emit(undefined, undefined, undefined, undefined, {
before: [optimizeForOf(tsProgram), optimizeArrayDestructuring],
after: [changeExtensionInImportPaths({ extension }), inlineInvariant],
});
assert(
Expand Down
29 changes: 29 additions & 0 deletions resources/optimize-array-destructuring.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import ts from 'typescript';

export const optimizeArrayDestructuring: ts.TransformerFactory<ts.SourceFile> = (context) => {
const { factory } = context;

return (sourceFile) => {
const visitor = (node: ts.Node): ts.Node => {
if(ts.isArrayBindingPattern(node)) {
const elements = node.elements.map((el, i) => {
if (!el.getText()) return undefined
return { key: '' + i, name: el.getText() }
}).filter(Boolean) as Array<{ key: string, name: string }>

const newElements = elements.map(el => {
const key = factory.createIdentifier(el.key)
const name = factory.createIdentifier(el.name)
return factory.createBindingElement(undefined, key, name)
})

const objectBindingPattern = factory.createObjectBindingPattern(newElements)
return objectBindingPattern
}

return ts.visitEachChild(node, visitor, context);
};
return ts.visitNode(sourceFile, visitor) as ts.SourceFile;
};
};

187 changes: 187 additions & 0 deletions resources/optimize-for-of.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import assert from 'node:assert';

import ts from 'typescript';

/**
* The following ES6 code:
*
* ```
* for (let v of expr) { }
* ```
*
* should be emitted as
*
* ```
* for (let _i = 0, _a = expr; _i < _a.length; _i++) {
* let v = _a[_i];
* }
* ```
*
* where _a and _i are temps emitted to capture the RHS and the counter, respectively.
* When the left hand side is a let/const, the v is renamed if there is another v in scope.
* Note that all assignments to the LHS are emitted in the body, including all destructuring.
*
* Code is based on TS ES5 transpilation:
* https://github.com/microsoft/TypeScript/blob/71e852922888337ef51a0e48416034a94a6c34d9/src/compiler/transformers/es2015.ts#L2521
*/
export const optimizeForOf = (program: ts.Program) => {
const transformer: ts.TransformerFactory<ts.SourceFile> = (context: ts.TransformationContext) => {
const typeChecker = program.getTypeChecker();
const { factory } = context;

return (node: ts.SourceFile) => {
return visitSourceFile(node) as ts.SourceFile;
};

function visitSourceFile(sourceFile: ts.SourceFile) {
return ts.visitNode(sourceFile, visitNode);

function visitNode(node: ts.Node): ts.Node {
if (isArrayForOfStatement(node)) {
return convertForOfStatementForArray(node);
}
return ts.visitEachChild(node, visitNode, context);
}

function isArrayForOfStatement(node: ts.Node): node is ts.ForOfStatement {
if (!ts.isForOfStatement(node) || node.awaitModifier != null) {
return false;
}

const { expression } = node;

const expressionType = typeChecker.getTypeAtLocation(expression);

for (const subType of unionTypeParts(expressionType)) {
assert(
!(subType.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)),
'Can not use any or uknown values in for-of loop: ' +
nodeLocationString(node),
);

if (subType.flags & ts.TypeFlags.StringLike) {
continue;
}

const typeName = subType.getSymbol()?.getName();
assert(typeName != null);

if (typeName === 'Array' || typeName === 'ReadonlyArray') {
continue;
}

return false;
}

return true;
}

function nodeLocationString(node: ts.Node): string {
const position = sourceFile.getLineAndCharacterOfPosition(
node.getStart(),
);
return sourceFile.fileName + ':' + position.line;
}

function unionTypeParts(type: ts.Type): ReadonlyArray<ts.Type> {
return isUnionType(type) ? type.types : [type];
}

function isUnionType(type: ts.Type): type is ts.UnionType {
return (type.flags & ts.TypeFlags.Union) !== 0;
}

function convertForOfStatementForArray(
forOfNode: ts.ForOfStatement,
): ts.Statement {
const counter = factory.createLoopVariable();
const forDeclarations = [
factory.createVariableDeclaration(
counter,
undefined, // exclamationToken
undefined, // type
factory.createNumericLiteral(0),
),
];

// In the case where the user wrote an identifier as the RHS, like this:
//
// for (let v of arr) { }
//
// we don't want to emit a temporary variable for the RHS, just use it directly.
let rhsReference;
if (ts.isIdentifier(forOfNode.expression)) {
rhsReference = forOfNode.expression;
} else {
rhsReference = factory.createTempVariable(
undefined, // recordTempVariable
);
forDeclarations.push(
factory.createVariableDeclaration(
rhsReference,
undefined, // exclamationToken
undefined, // type
forOfNode.expression,
),
);
}

const forIntiliazer = factory.createVariableDeclarationList(
forDeclarations,
ts.NodeFlags.Let,
);

const forCondition = factory.createLessThan(
counter,
factory.createPropertyAccessExpression(rhsReference, 'length'),
);
const forIncrementor = factory.createPostfixIncrement(counter);

assert(ts.isVariableDeclarationList(forOfNode.initializer));
// It will use rhsIterationValue _a[_i] as the initializer.
const itemAssigment = convertForOfInitializer(
forOfNode.initializer,
factory.createElementAccessExpression(rhsReference, counter),
);

assert(ts.isBlock(forOfNode.statement));
const forBody = factory.updateBlock(forOfNode.statement, [
itemAssigment,
...forOfNode.statement.statements,
]);

return factory.createForStatement(
forIntiliazer,
forCondition,
forIncrementor,
forBody,
);
}

function convertForOfInitializer(
forOfDeclarationList: ts.VariableDeclarationList,
itemAccessExpression: ts.Expression,
) {
assert(forOfDeclarationList.declarations.length === 1);
const [forOfDeclaration] = forOfDeclarationList.declarations;

const updatedDeclaration = factory.updateVariableDeclaration(
forOfDeclaration,
forOfDeclaration.name,
forOfDeclaration.exclamationToken,
forOfDeclaration.type,
itemAccessExpression,
);

return factory.createVariableStatement(
undefined, // modifiers
factory.updateVariableDeclarationList(forOfDeclarationList, [
updatedDeclaration,
]),
);
}
}
};

return transformer;
}
2 changes: 1 addition & 1 deletion src/language/visitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ export function visit(
let inArray = Array.isArray(root);
let keys: any = [root];
let index = -1;
let edits = [];
let edits: Array<any> = [];
let node: any = root;
let key: any = undefined;
let parent: any = undefined;
Expand Down