-
Notifications
You must be signed in to change notification settings - Fork 887
RuleWalker-based rules are slow and should be avoided #2522
Comments
@RyanCavanaugh thanks for the heads-up. We've been converting a lot of rules to use the recur-style functional pattern recently (props to @andy-hanson and @ajafff) and have written up performance tips and walker design docs that started from the investigation in #1131. I found that TSLint v5 was almost 2x as fast as v4 in a few projects I ran it on (using the recommended config preset), so I think we're well on our way to better linting performance. This is a good issue thread to collect general advice for developers. Once we migrate all the core rules away from RuleWalker, we can deprecate it and hopefully remove it completely a couple major versions from now. |
I was thinking about writing some single-pass rule setup. We could have something like this, where many rules could share a single tree walk export class Rule extends Lint.Rules.AbstractRule {
public static FAILURE_STRING = "Class name must be in pascal case";
public applyFast(walkContext: WalkContext, sourceFile: ts.SourceFile): Lint.RuleFailure[] {
// walkContext provides 'on', 'before', and 'after' so rules can do some
// nested declaration investigation
walkContext.on([ts.SyntaxKind.ClassDeclaration, ts.SyntaxKind.ClassExpression], checkClass);
walkContext.on([ts.SyntaxKind.InterfaceDeclaration], checkInterface);
}
}
function checkClass(context: WalkContext, node: ts.ClassDeclaration | ts.ClassExpression) {
// check node.name
}
function checkInterface(context: WalkContext, node: ts.InterfaceDeclaration) {
// check node.name
context.addError(node, Rule.FAILURE_STRING);
} Thoughts? For most things which only have to check some identifiers or specific keywords, this could be a substantial improvement |
Ah, sorry I didn't address the last part of your OP in my above comment. A single-pass rule setup for a subset of the core rules sounds great too 👍 |
Prototype branch in progress master...RyanCavanaugh:fastLint I'm converting over the rules the TS repo uses as a first stab. So far it's working quite nicely |
TL;DR: please don't, maybe I have an alternative Although I like the idea, I don't think you will gain that much performance. I thought about this some time ago and figured out that it will eventually have the same performance problems that we see in
Instead of writing a more efficient algorithm to work around the performance problems of typescript's AST, why not simply use other data structures that enable better algorithms? I wrote a small PoC that walks the whole AST once and converts it into two formats which are then cached in a WeakMap. https://github.com/ajafff/tsutils/blob/wrap-ast/src/wrap.ts
This could be used by all rules and will even get faster because of the cache. Open questions related to caching:
|
@ajafff interesting idea! Some thoughts
We have to start by correctly identifying the problem - the AST is not slow! It's nearly as fast as an implementation could plausibly be. It's just the fact that for a large project, an AST walk involves hundreds of thousands of calls. As a baseline, here are the times it took to run our rules on our codebase (I have manually sorted this list):
There's a clear delineation: Rules which walk everything incur a minimum ~250ms cost, which is just the baseline cost to do an entire walk. As I converted rules over, it was easy to see that cost vanish (because the non-walk cost of most rules is extremely small).
I can't see how this is avoidable?
Falling back to an iterative mode if more than one rule produces fixes is fine. The modal number of fixes is very small. At worst, it's equivalent to today's implementation.
These function calls aren't virtual (there's no prototype chain to walk). They're indirect, but have fixed lexical/
I don't see how it's any different from today. I like the approach I have here because we can easily convert all existing walk-based rules to it with no loss of generality and very little risk (see the diff I linked). But I also like yours because I agree it ought to be very easy to write a rule that only scans certain node kinds. One thing I'm not clear on with your approach: When will walking a tree once in order to make a new data structure, then checking against that data structure, be cheaper than doing those same checks during the walk? It seems like the former will be faster for complex rules walking small trees, where the latter will be faster for simple rules walking large trees. What does the balance of rules actually look like? It'd be great to run an apples-to-apples comparison. Since the future will certainly involve rules that need to participate in a full (hopefully single) walk, and rules which want to just query select nodes... por que no los dos? We just have no reason to have rules which run their own full walk. |
@ajafff What can be done to make something happen here? The shared walker approach can get us massive perf gains with only minimal code updates to existing rules. I don't want this to be a "licked cookie" blocked by an alternate approach that isn't making forward progress. |
@RyanCavanaugh could you provide a hint how you created those numbers? |
Hey @karfau I happened to also have this need - see...
You could create a |
Bug Report
Rules using the built-in
RuleWalker
class are more than twice as slow as those using a more efficient pattern. Either some substantial refactoring should be done to make that class more efficient (not sure if that's possible), or it should be deprecated for rules which don't actually need its functionality and existing rules converted over.master
master
TypeScript code being linted
https://www.github.com/Microsoft/TypeScript (via
jake lint
)Actual behavior
Adding some code to sum up the time needed by each rule, on my machine, the
classNameRule
(class-name
) takes an average of ~900ms to run.Converting this rule to a
recur
-style functional pattern drops that time to ~300msLet's compare the old code to the new code:
https://github.com/palantir/tslint/blob/master/src/rules/classNameRule.ts (trimmed to the relevant portion):
New code:
Analysis
I think the root cause here is just the large number of indirections needed to complete a tree walk. The VM can't possibly inline or JIT all these virtual method calls, but the closed-over form can be more efficiently optimized.
An even better solution would be if all (or most) rules could share in a single pass of the tree. TypeScript itself budgets full-tree walks very carefully (the binder does ~three and the checker does ~two) because it's so slow for larger programs.
The text was updated successfully, but these errors were encountered: