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
87 changes: 87 additions & 0 deletions bench/Source/NestedTryCatchSource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace Rector\Bench\Source;

use InvalidArgumentException;
use RuntimeException;
use Throwable;

final class NestedTryCatchSource
{
public function run(array $items): int
{
$count = 0;

foreach ($items as $item) {
try {
$count += $this->process($item);
} catch (InvalidArgumentException $invalidArgumentException) {
try {
$count += $this->retry($item, $invalidArgumentException->getMessage());
} catch (RuntimeException $runtimeException) {
try {
$count += $this->fallback($item, $runtimeException->getCode());
} catch (Throwable $throwable) {
try {
$this->log($throwable->getMessage() . ' ' . $item);
} catch (Throwable) {
$count -= 1;
}
}
}
}
}

return $count;
}

public function sequential(string $value): string
{
try {
$value = $this->normalize($value);
} catch (InvalidArgumentException $invalidArgumentException) {
$value = $invalidArgumentException->getMessage();
}

try {
$value = strtoupper($this->normalize($value));
} catch (RuntimeException $runtimeException) {
try {
$value = $this->normalize($runtimeException->getMessage());
} catch (Throwable) {
$value = '';
}
} finally {
$value .= '!';
}

return $value;
}

private function process(mixed $item): int
{
return is_int($item) ? $item : throw new InvalidArgumentException('not int');
}

private function retry(mixed $item, string $reason): int
{
return is_numeric($item) ? (int) $item : throw new RuntimeException($reason);
}

private function fallback(mixed $item, int $code): int
{
return $code + (is_string($item) ? strlen($item) : 0);
}

private function log(string $message): void
{
error_log($message);
}

private function normalize(string $value): string
{
return trim($value) !== '' ? trim($value) : throw new InvalidArgumentException('empty');
}
}
56 changes: 56 additions & 0 deletions bench/php_stan_node_scope_resolver_catch_bench.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

/**
* Benchmark for PHPStanNodeScopeResolver::processNodes() on try/catch heavy code.
*
* PHPStan already fills the catch stmts with the catch scope, but every Catch_ callback recursed
* into processNodes() for the catch stmts again - which also re-ran the decorating node visitors on
* them. With nested try/catch, each level doubled the work of the levels inside it.
*
* The change only processes the catch stmts PHPStan skipped (no matching throw point), after the
* PHPStan walk, and runs the decorating node visitors once over the whole tree.
*
* Run on both branches to compare:
* php bench/php_stan_node_scope_resolver_catch_bench.php
*
* Reports CPU time (user+sys) and wall time; CPU is the reliable metric, wall is noisy under
* concurrent load.
*/

use Rector\DependencyInjection\LazyContainerFactory;
use Rector\Testing\TestingParser\TestingParser;

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

const REPEAT = 200;

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

/** @var TestingParser $testingParser */
$testingParser = $rectorConfig->make(TestingParser::class);

$sourceFilePath = __DIR__ . '/Source/NestedTryCatchSource.php';

$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 autoloading / reflection once, excluded from timing
$testingParser->parseFileToDecoratedNodes($sourceFilePath);

$cpuStart = $cpuSeconds();
$wallStart = hrtime(true);

for ($repeat = 0; $repeat < REPEAT; ++$repeat) {
$testingParser->parseFileToDecoratedNodes($sourceFilePath);
}

printf('PHPStanNodeScopeResolver::processNodes() nested try/catch benchmark%s', PHP_EOL);
printf(' repeat=%d%s', REPEAT, PHP_EOL);
printf('%s', PHP_EOL);
printf(' cpu=%.4fs wall=%.4fs%s', $cpuSeconds() - $cpuStart, (hrtime(true) - $wallStart) / 1_000_000_000, PHP_EOL);
41 changes: 33 additions & 8 deletions src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,26 @@ public function processNodes(

$scope = $formerMutatingScope ?? $this->scopeFactory->createFromFile($filePath);

$this->fillScope($stmts, $filePath, $scope);

// use after scope filling so DecoratingNodeVisitorInterface instance can fetch the scope of target node
// @see https://github.com/rectorphp/rector-src/pull/7721#discussion_r2595932460
$this->nodeTraverser->traverse($stmts);

return $stmts;
}

/**
* @param Stmt[] $stmts
*/
private function fillScope(array $stmts, string $filePath, MutatingScope $scope): void
{
/** @var array<array{Catch_, MutatingScope}> $catches */
$catches = [];

$nodeCallback = function (Node $node, MutatingScope $mutatingScope) use (
&$nodeCallback,
$filePath,
&$catches,
): void {
if ($mutatingScope instanceof NodeCallbackScope) {
$mutatingScope = $mutatingScope->toWalkScope();
Expand Down Expand Up @@ -312,7 +329,13 @@ public function processNodes(
}

if ($node instanceof Catch_) {
$this->processCatch($node, $filePath, $mutatingScope);
// PHPStan fills the catch stmts itself right after this callback, unless no throw point matches;
// reset the first stmt scope, so the skipped ones can be detected after processing
if ($node->stmts !== []) {
$node->stmts[0]->setAttribute(AttributeKey::SCOPE, null);
$catches[] = [$node, $mutatingScope];
}

return;
}

Expand Down Expand Up @@ -398,11 +421,9 @@ public function processNodes(
RectorNodeScopeResolver::processNodes($stmts, $scope);
}

// use after scope filling so DecoratingNodeVisitorInterface instance can fetch the scope of target node
// @see https://github.com/rectorphp/rector-src/pull/7721#discussion_r2595932460
$this->nodeTraverser->traverse($stmts);

return $stmts;
foreach ($catches as [$catch, $catchScope]) {
$this->processCatch($catch, $filePath, $catchScope);
}
}

private function processYield(Yield_ $yield, MutatingScope $mutatingScope): void
Expand Down Expand Up @@ -532,6 +553,10 @@ private function processSwitch(Switch_ $switch, MutatingScope $mutatingScope): v

private function processCatch(Catch_ $catch, string $filePath, MutatingScope $mutatingScope): void
{
if ($catch->stmts[0]->getAttribute(AttributeKey::SCOPE) instanceof MutatingScope) {
return;
}

$varName = $catch->var instanceof Variable
? $this->nodeNameResolver->getName($catch->var)
: null;
Expand All @@ -541,7 +566,7 @@ private function processCatch(Catch_ $catch, string $filePath, MutatingScope $mu
);

$catchMutatingScope = $mutatingScope->enterCatchType($type, $varName);
$this->processNodes($catch->stmts, $filePath, $catchMutatingScope);
$this->fillScope($catch->stmts, $filePath, $catchMutatingScope);
}

private function processTryCatch(TryCatch $tryCatch, MutatingScope $mutatingScope): void
Expand Down
Loading