From 14c1ea36fde301c32669816e5c7a2c3a99f5b34e Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 19 Nov 2025 10:08:46 +0000 Subject: [PATCH] add simple node lookup script --- scripts/bench/fixture/SomeSnippet.php | 15 + scripts/bench/node-lookup-vs-instanceof.php | 443 ++++++++++++++++++ .../NodeTraverser/RectorNodeTraverser.php | 1 - 3 files changed, 458 insertions(+), 1 deletion(-) create mode 100644 scripts/bench/fixture/SomeSnippet.php create mode 100644 scripts/bench/node-lookup-vs-instanceof.php diff --git a/scripts/bench/fixture/SomeSnippet.php b/scripts/bench/fixture/SomeSnippet.php new file mode 100644 index 00000000000..c809c9dca85 --- /dev/null +++ b/scripts/bench/fixture/SomeSnippet.php @@ -0,0 +1,15 @@ + [Arg::class], + ArrayItem::class => [ArrayItem::class], + ComplexType::class => [IntersectionType::class, UnionType::class], + Expr::class => [ + ArrayDimFetch::class, + Array_::class, + \PhpParser\Node\Expr\ArrayItem::class, + Assign::class, + AssignOp::class, + AssignRef::class, + BinaryOp::class, + BooleanNot::class, + Cast::class, + //\PhpParser\Node\Expr\Clone_:class, + Closure::class, + ClosureUse::class, + ConstFetch::class, + //\PhpParser\Node\Expr\Empty_:class, + ErrorSuppress::class, + Exit_::class, + FuncCall::class, + //\PhpParser\Node\Expr\Include_:class, + Instanceof_::class, + //\PhpParser\Node\Expr\Isset_:class, + List_::class, + //\PhpParser\Node\Expr\MathError::class, + Match_::class, + MethodCall::class, + New_::class, + NullsafeMethodCall::class, + NullsafePropertyFetch::class, + PostDec::class, + PostInc::class, + PreDec::class, + PreInc::class, + Print_::class, + PropertyFetch::class, + ShellExec::class, + StaticCall::class, + StaticPropertyFetch::class, + Ternary::class, + Throw_::class, + UnaryMinus::class, + UnaryPlus::class, + Variable::class, + Yield_::class, + YieldFrom::class, + ArrowFunction::class, + ], + AssignOp::class => [ + BitwiseAnd::class, + BitwiseOr::class, + BitwiseXor::class, + Coalesce::class, + Concat::class, + Div::class, + Minus::class, + Mod::class, + Mul::class, + Plus::class, + Pow::class, + ShiftLeft::class, + ShiftRight::class, + ], + BinaryOp::class => [ + \PhpParser\Node\Expr\BinaryOp\BitwiseAnd::class, + \PhpParser\Node\Expr\BinaryOp\BitwiseOr::class, + \PhpParser\Node\Expr\BinaryOp\BitwiseXor::class, + BooleanAnd::class, + BooleanOr::class, + \PhpParser\Node\Expr\BinaryOp\Coalesce::class, + \PhpParser\Node\Expr\BinaryOp\Concat::class, + \PhpParser\Node\Expr\BinaryOp\Div::class, + Equal::class, + Greater::class, + GreaterOrEqual::class, + Identical::class, + LogicalAnd::class, + LogicalOr::class, + LogicalXor::class, + \PhpParser\Node\Expr\BinaryOp\Minus::class, + \PhpParser\Node\Expr\BinaryOp\Mod::class, + \PhpParser\Node\Expr\BinaryOp\Mul::class, + NotEqual::class, + NotIdentical::class, + \PhpParser\Node\Expr\BinaryOp\Plus::class, + \PhpParser\Node\Expr\BinaryOp\Pow::class, + \PhpParser\Node\Expr\BinaryOp\ShiftLeft::class, + \PhpParser\Node\Expr\BinaryOp\ShiftRight::class, + Smaller::class, + SmallerOrEqual::class, + Spaceship::class, + ], + MatchArm::class => [MatchArm::class], + Name::class => [Name::class, FullyQualified::class, Relative::class], + Param::class => [Param::class], + Scalar::class => [ + DNumber::class, + Encapsed::class, + EncapsedStringPart::class, + LNumber::class, + MagicConst::class, + String_::class, + ], + MagicConst::class => [ + Class_::class, + Dir::class, + File::class, + Function_::class, + Line::class, + Method::class, + Namespace_::class, + Trait_::class, + ], + Stmt::class => [ + Break_::class, + Case_::class, + Catch_::class, + \PhpParser\Node\Stmt\Class_::class, + ClassConst::class, + ClassMethod::class, + Const_::class, + Continue_::class, + Declare_::class, + Do_::class, + Echo_::class, + Enum_::class, + Expression::class, + Finally_::class, + For_::class, + Foreach_::class, + \PhpParser\Node\Stmt\Function_::class, + Global_::class, + Goto_::class, + GroupUse::class, + HaltCompiler::class, + If_::class, + InlineHTML::class, + Interface_::class, + Label::class, + \PhpParser\Node\Stmt\Namespace_::class, + Property::class, + PropertyHook::class, + Return_::class, + Static_::class, + StaticVar::class, + Switch_::class, + \PhpParser\Node\Stmt\Throw_::class, + \PhpParser\Node\Stmt\Trait_::class, + TraitUse::class, + TryCatch::class, + Unset_::class, + Use_::class, + UseUse::class, + While_::class, + ], + ClassLike::class => [ + \PhpParser\Node\Stmt\Class_::class, + Interface_::class, + \PhpParser\Node\Stmt\Trait_::class, + Enum_::class, + ], + FunctionLike::class => [ + ClassMethod::class, + \PhpParser\Node\Stmt\Function_::class, + Closure::class, + ArrowFunction::class, + \PhpParser\Node\PropertyHook::class, + ], + ]; +} + +$iterations = 10000; // number of traversals per run (set lower if file is big) +$runs = 10; + +// ----------------------------------------------------------------------------- +// 1. Helpers +// ----------------------------------------------------------------------------- + +/** + * Static table version – generic => list of concrete nodes + */ +function isFunctionLikeTable(Node $node): bool +{ + return in_array($node::class, Map::NODE_INSTANCE_TO_TYPE_MAP[FunctionLike::class] ?? [], true); +} + +function average(array $values): float +{ + return array_sum($values) / count($values); +} + +// global sink to avoid dead-code elimination +$GLOBALS['__bench_sink'] = false; + +// ----------------------------------------------------------------------------- +// 2. Prepare nodes +// ----------------------------------------------------------------------------- + +// real nodes from a real file +$phpParserFactory = new ParserFactory(); +$phpParser = $phpParserFactory->createForHostVersion(); + +$stmts = $phpParser->parse(file_get_contents(__DIR__ . '/fixture/SomeSnippet.php')); + +$nodeTraverser = new NodeTraverser(); + +final class CheckPerformanceNodeVisitor extends NodeVisitorAbstract +{ + /** + * @var callable(Node): void + */ + public $tester; + + public function enterNode(Node $node): void + { + // test here + ($this->tester)($node); + } +} + +$visitor = new CheckPerformanceNodeVisitor(); +$nodeTraverser->addVisitor($visitor); + +// ----------------------------------------------------------------------------- +// 3. Benchmark helper +// ----------------------------------------------------------------------------- + +/** + * @param callable(Node): void $callback + */ +function run_benchmark( + NodeTraverser $nodeTraverser, + CheckPerformanceNodeVisitor $checkPerformanceNodeVisitor, + callable $callback, + array $stmts, + int $iterations +): float { + $checkPerformanceNodeVisitor->tester = $callback; + + $start = hrtime(true); + + for ($i = 0; $i < $iterations; ++$i) { + $nodeTraverser->traverse($stmts); + } + + $elapsed = hrtime(true) - $start; + + // return ns per traversal + return $elapsed / $iterations; +} + +// ----------------------------------------------------------------------------- +// 4. Benchmark with multiple runs +// ----------------------------------------------------------------------------- + +$tableDurations = []; +$isADurations = []; + +for ($run = 0; $run < $runs; ++$run) { + // Static table + $tableDurations[] = run_benchmark( + $nodeTraverser, + $visitor, + static function (Node $node): void { + // static table lookup + $GLOBALS['__bench_sink'] ^= isFunctionLikeTable($node); + }, + $stmts, + $iterations + ); + + // is_a() with true + $isADurations[] = run_benchmark( + $nodeTraverser, + $visitor, + static function (Node $node): void { + $GLOBALS['__bench_sink'] ^= $node instanceof FunctionLike; + }, + $stmts, + $iterations + ); +} + +// ----------------------------------------------------------------------------- +// 5. Output +// ----------------------------------------------------------------------------- + +echo sprintf('Traversals per run: %d%s', $iterations, PHP_EOL); +echo "Runs: {$runs}\n\n"; + +echo "Average time per traversed file (nanoseconds):\n"; +echo 'Static table: ' . average($tableDurations) . " ns\n"; +echo 'is_a() check: ' . average($isADurations) . " ns\n"; + +if (average($isADurations) > 0) { + echo "\nRatio (table / is_a): " . (average($tableDurations) / average($isADurations)) . "\n"; +} + +echo "\nIgnore sink: " . (int) $GLOBALS['__bench_sink'] . "\n"; diff --git a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php index 1cb2e5ebd00..cf8767af854 100644 --- a/src/PhpParser/NodeTraverser/RectorNodeTraverser.php +++ b/src/PhpParser/NodeTraverser/RectorNodeTraverser.php @@ -71,7 +71,6 @@ public function getVisitorsForNode(Node $node): array if (! isset($this->visitorsPerNodeClass[$nodeClass])) { $this->visitorsPerNodeClass[$nodeClass] = []; foreach ($this->visitors as $visitor) { - assert($visitor instanceof RectorInterface); foreach ($visitor->getNodeTypes() as $nodeType) { if (is_a($nodeClass, $nodeType, true)) { $this->visitorsPerNodeClass[$nodeClass][] = $visitor;