Skip to content
Merged
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
106 changes: 106 additions & 0 deletions bench/context_node_visitor_bench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

/**
* Benchmark for ContextNodeVisitor.
*
* The visitor runs inside the decorating traversal, yet it started a nested traversal of its own
* for every loop/switch (to mark If_/Break_ in the body), every try block (to mark the try stmts)
* and every attribute (to mark Array_/Closure in its args). Nested loops and try blocks are
* re-walked once per enclosing loop/try, and each try block built a new NodeTraverser plus an
* anonymous visitor.
*
* The optimization tracks loop/try/attribute depth in enterNode()/leaveNode() of the main
* traversal instead, so every node is visited once.
*
* This bench parses a class with methods full of nested loops, switch, try/catch and attributes,
* then traverses it with ContextNodeVisitor only.
*
* Run on both branches to compare:
* php bench/context_node_visitor_bench.php
*
* Reports CPU time (user+sys) and wall time; CPU is the reliable metric, wall is noisy
* under concurrent load.
*/

use PhpParser\NodeTraverser;
use PhpParser\ParserFactory;
use Rector\DependencyInjection\LazyContainerFactory;
use Rector\PhpParser\NodeVisitor\ContextNodeVisitor;

require __DIR__ . '/../vendor/autoload.php';

const METHODS = 60; // methods, each with nested loops, switch, try/catch
const REPEAT = 200; // outer repeats over the whole file

$rectorConfig = new LazyContainerFactory()->create();
$rectorConfig->boot();

$contextNodeVisitor = $rectorConfig->make(ContextNodeVisitor::class);

$source = "<?php\n\nnamespace Bench;\n\nclass SomeClass\n{\n";
for ($i = 0; $i < METHODS; ++$i) {
$source .= " #[Route(path: '/run{$i}', methods: ['GET', 'POST'], options: ['a' => [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);
140 changes: 95 additions & 45 deletions src/PhpParser/NodeVisitor/ContextNodeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<array{int, int}>
*/
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;
}

Expand All @@ -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;
}

Expand All @@ -87,7 +111,7 @@ public function enterNode(Node $node): ?Node
}

if ($node instanceof Attribute) {
$this->processContextInAttribute($node);
++$this->attributeDepth;
return null;
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
}
);
}
}
Loading