From 9110a21e2ee10f932c0b1b75a59ba4ab15cec678 Mon Sep 17 00:00:00 2001 From: Abdul Malik Ikhsan Date: Wed, 21 Oct 2026 00:43:21 +0700 Subject: [PATCH] [perf] Avoid nested traversals in ContextNodeVisitor by tracking loop/try/attribute depth --- bench/context_node_visitor_bench.php | 106 +++++++++++++ .../NodeVisitor/ContextNodeVisitor.php | 140 ++++++++++++------ 2 files changed, 201 insertions(+), 45 deletions(-) create mode 100644 bench/context_node_visitor_bench.php diff --git a/bench/context_node_visitor_bench.php b/bench/context_node_visitor_bench.php new file mode 100644 index 00000000000..0b9f0a802fc --- /dev/null +++ b/bench/context_node_visitor_bench.php @@ -0,0 +1,106 @@ +create(); +$rectorConfig->boot(); + +$contextNodeVisitor = $rectorConfig->make(ContextNodeVisitor::class); + +$source = " [1, 2]])]\n"; + $source .= " public function run{$i}(array \$items, int \$mode)\n {\n"; + $source .= " try {\n"; + $source .= " foreach (\$items as \$key => \$item) {\n"; + $source .= " foreach (\$item as \$value) {\n"; + $source .= " if (\$value === null) {\n"; + $source .= " break;\n"; + $source .= " }\n"; + $source .= " for (\$j = 0; \$j < 3; ++\$j) {\n"; + $source .= " switch (\$mode) {\n"; + $source .= " case 1:\n"; + $source .= " \$this->handle(\$key, \$value, [\$j, \$mode]);\n"; + $source .= " break;\n"; + $source .= " default:\n"; + $source .= " if (\$j > 1) {\n"; + $source .= " break 2;\n"; + $source .= " }\n"; + $source .= " }\n"; + $source .= " }\n"; + $source .= " }\n"; + $source .= " }\n"; + $source .= " try {\n"; + $source .= " \$result = \$this->compute(\$items[0] ?? null, \$mode + 1);\n"; + $source .= " } catch (\\RuntimeException \$e) {\n"; + $source .= " \$result = null;\n"; + $source .= " }\n"; + $source .= " } catch (\\Throwable \$throwable) {\n"; + $source .= " return null;\n"; + $source .= " } finally {\n"; + $source .= " \$this->cleanup();\n"; + $source .= " }\n\n"; + $source .= " return \$result;\n"; + $source .= " }\n\n"; +} + +$source .= "}\n"; + +$stmts = new ParserFactory()->createForHostVersion()->parse($source); + +$nodeTraverser = new NodeTraverser($contextNodeVisitor); + +$cpuSeconds = static function (): float { + $usage = getrusage(); + return $usage['ru_utime.tv_sec'] + $usage['ru_utime.tv_usec'] / 1_000_000 + + $usage['ru_stime.tv_sec'] + $usage['ru_stime.tv_usec'] / 1_000_000; +}; + +// warm up once, excluded from timing +$nodeTraverser->traverse($stmts); + +$wallStart = microtime(true); +$cpuStart = $cpuSeconds(); + +for ($repeat = 0; $repeat < REPEAT; ++$repeat) { + $nodeTraverser->traverse($stmts); +} + +$cpu = $cpuSeconds() - $cpuStart; +$wall = microtime(true) - $wallStart; + +printf('methods: %d, repeats: %d%s', METHODS, REPEAT, PHP_EOL); +printf('CPU: %.4f s (%.3f ms/traverse)%s', $cpu, $cpu / REPEAT * 1000, PHP_EOL); +printf('wall: %.4f s%s', $wall, PHP_EOL); diff --git a/src/PhpParser/NodeVisitor/ContextNodeVisitor.php b/src/PhpParser/NodeVisitor/ContextNodeVisitor.php index d3652bdff79..b3ec45d146b 100644 --- a/src/PhpParser/NodeVisitor/ContextNodeVisitor.php +++ b/src/PhpParser/NodeVisitor/ContextNodeVisitor.php @@ -21,41 +21,66 @@ use PhpParser\Node\Expr\PropertyFetch; use PhpParser\Node\Expr\StaticPropertyFetch; use PhpParser\Node\Expr\Variable; +use PhpParser\Node\FunctionLike; use PhpParser\Node\Identifier; use PhpParser\Node\Name\FullyQualified; use PhpParser\Node\Param; use PhpParser\Node\Scalar\String_; use PhpParser\Node\Stmt\Break_; +use PhpParser\Node\Stmt\Catch_; use PhpParser\Node\Stmt\Class_; use PhpParser\Node\Stmt\Do_; use PhpParser\Node\Stmt\Else_; use PhpParser\Node\Stmt\ElseIf_; +use PhpParser\Node\Stmt\Finally_; use PhpParser\Node\Stmt\For_; use PhpParser\Node\Stmt\Foreach_; -use PhpParser\Node\Stmt\Function_; use PhpParser\Node\Stmt\If_; use PhpParser\Node\Stmt\Switch_; use PhpParser\Node\Stmt\TryCatch; use PhpParser\Node\Stmt\Unset_; use PhpParser\Node\Stmt\While_; -use PhpParser\NodeVisitor; use PhpParser\NodeVisitorAbstract; use Rector\Contract\PhpParser\DecoratingNodeVisitorInterface; use Rector\NodeTypeResolver\Node\AttributeKey; -use Rector\PhpDocParser\NodeTraverser\SimpleCallableNodeTraverser; -use Rector\PhpParser\NodeTraverser\SimpleNodeTraverser; final class ContextNodeVisitor extends NodeVisitorAbstract implements DecoratingNodeVisitorInterface { - public function __construct( - private readonly SimpleCallableNodeTraverser $simpleCallableNodeTraverser - ) { + /** + * Context is tracked by depth during the main traversal, instead of traversing + * the loop, try and attribute children again on every enter + */ + private int $loopDepth = 0; + + private int $tryDepth = 0; + + private int $attributeDepth = 0; + + /** + * @var array + */ + private array $outerLoopAndTryDepths = []; + + /** + * @param Node[] $nodes + */ + public function beforeTraverse(array $nodes): null + { + $this->loopDepth = 0; + $this->tryDepth = 0; + $this->attributeDepth = 0; + $this->outerLoopAndTryDepths = []; + + return null; } public function enterNode(Node $node): ?Node { + $this->decorateEnclosingContext($node); + if ($node instanceof For_ || $node instanceof Foreach_ || $node instanceof While_ || $node instanceof Do_ || $node instanceof Switch_) { $this->processContextInLoop($node); + ++$this->loopDepth; return null; } @@ -73,8 +98,7 @@ public function enterNode(Node $node): ?Node } if ($node instanceof TryCatch) { - SimpleNodeTraverser::decorateWithAttributeValue($node->stmts, AttributeKey::IS_IN_TRY_BLOCK, true); - + ++$this->tryDepth; return null; } @@ -87,7 +111,7 @@ public function enterNode(Node $node): ?Node } if ($node instanceof Attribute) { - $this->processContextInAttribute($node); + ++$this->attributeDepth; return null; } @@ -134,6 +158,67 @@ public function enterNode(Node $node): ?Node return null; } + public function leaveNode(Node $node): null + { + if ($node instanceof For_ || $node instanceof Foreach_ || $node instanceof While_ || $node instanceof Do_ || $node instanceof Switch_) { + --$this->loopDepth; + return null; + } + + if ($node instanceof TryCatch) { + --$this->tryDepth; + return null; + } + + // back to the try stmts of the parent TryCatch, see decorateEnclosingContext() + if ($node instanceof Catch_ || $node instanceof Finally_) { + ++$this->tryDepth; + return null; + } + + if ($node instanceof Attribute) { + --$this->attributeDepth; + return null; + } + + if ($node instanceof Class_ || $node instanceof FunctionLike) { + [$this->loopDepth, $this->tryDepth] = array_pop($this->outerLoopAndTryDepths) ?? [0, 0]; + } + + return null; + } + + private function decorateEnclosingContext(Node $node): void + { + // catch and finally are not part of the try block of their parent TryCatch + if ($node instanceof Catch_ || $node instanceof Finally_) { + --$this->tryDepth; + } + + // nested class or function-like does not belong to outer loop or try block + if ($node instanceof Class_ || $node instanceof FunctionLike) { + $this->outerLoopAndTryDepths[] = [$this->loopDepth, $this->tryDepth]; + $this->loopDepth = 0; + $this->tryDepth = 0; + } elseif ($this->tryDepth > 0) { + $node->setAttribute(AttributeKey::IS_IN_TRY_BLOCK, true); + } + + if ($this->loopDepth > 0 && ($node instanceof If_ || $node instanceof Break_)) { + $node->setAttribute(AttributeKey::IS_IN_LOOP_OR_SWITCH, true); + } + + if ($this->attributeDepth > 0) { + if ($node instanceof Array_) { + $node->setAttribute(AttributeKey::IS_ARRAY_IN_ATTRIBUTE, true); + } + + if ($node instanceof Closure) { + $node->setAttribute(AttributeKey::IS_CLOSURE_IN_ATTRIBUTE, true); + } + } + } + private function processInsideArrayDimFetch(ArrayDimFetch $arrayDimFetch): void { if ($arrayDimFetch->var instanceof PropertyFetch || $arrayDimFetch->var instanceof StaticPropertyFetch) { @@ -203,24 +288,6 @@ private function processSetFactoryArgument(MethodCall $methodCall): void } } - private function processContextInAttribute(Attribute $attribute): void - { - $this->simpleCallableNodeTraverser->traverseNodesWithCallable( - $attribute->args, - static function (Node $subNode): null { - if ($subNode instanceof Array_) { - $subNode->setAttribute(AttributeKey::IS_ARRAY_IN_ATTRIBUTE, true); - } - - if ($subNode instanceof Closure) { - $subNode->setAttribute(AttributeKey::IS_CLOSURE_IN_ATTRIBUTE, true); - } - - return null; - } - ); - } - private function processContextInIf(If_|Else_|ElseIf_ $node): void { foreach ($node->stmts as $stmt) { @@ -239,22 +306,5 @@ private function processContextInLoop(For_|Foreach_|While_|Do_|Switch_ $node): v $node->valueVar->setAttribute(AttributeKey::IS_VARIABLE_LOOP, true); } - - $stmts = $node instanceof Switch_ ? $node->cases : $node->stmts; - - $this->simpleCallableNodeTraverser->traverseNodesWithCallable( - $stmts, - static function (Node $subNode): ?int { - if ($subNode instanceof Class_ || $subNode instanceof Function_ || $subNode instanceof Closure) { - return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN; - } - - if ($subNode instanceof If_ || $subNode instanceof Break_) { - $subNode->setAttribute(AttributeKey::IS_IN_LOOP_OR_SWITCH, true); - } - - return null; - } - ); } }