Skip to content

[perf] Speed up RemoveUnusedPrivateMethodRector with early data provider and class-string call checks and deduplicated scope resolution - #8800

Merged
TomasVotruba merged 1 commit into
rectorphp:mainfrom
alfredbez:fix/rector-perf-remove-unused-private-method-guard
Oct 25, 2026
Merged

TomasVotruba merged 1 commit into
rectorphp:mainfrom
alfredbez:fix/rector-perf-remove-unused-private-method-guard

Conversation

@alfredbez

@alfredbez alfredbez commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

In RemoveUnusedPrivateMethodRector, IsClassMethodUsedAnalyzer::isClassMethodUsed() (which traverses the class AST) was invoked for private methods before checking whether the method is a PHPUnit data provider or target of a class-string static call (self::class::method()). In addition, ScopeFetcher::fetch($node) was called repeatedly on the same Class_ node for each private method.

Reproducer

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;

final class Subject extends TestCase
{
    #[DataProvider('provideData')]
    public function testSomething(int $value): void
    {
        self::assertSame($value, self::class::computeExpected());
    }

    private static function provideData(): array
    {
        return [[1]];
    }

    private static function computeExpected(): int
    {
        return 1;
    }
}

Expected Behavior

RemoveUnusedPrivateMethodRector should check known-used method name lists (dataProviderMethodNames and classStringCallMethodNames) before running AST usage analysis, and fetch the class scope lazily at most once per class node.

Before / After

bench/remove_unused_private_method_bench.php (included in this PR): 150 TestCase classes, each with a private data provider, a private method called via self::class::method() and a locally used private method; 100 repeats = 15,000 refactor() calls, parsing and warmup excluded, CPU via getrusage().

Before (e27ea90, parent) After (a7b0485, this PR)
CPU, median of 3 7.26 s 1.76 s
CPU per call 484 us 117 us
Single runs 6.86 / 7.48 / 7.26 s 1.64 / 1.76 / 1.91 s
Process peak RSS ~120 MiB ~120 MiB

-75.7% CPU (~4.1x) for this case. Memory is unchanged.

Both revisions pass the same rule tests (49 tests, 51 assertions), including the new mixed_protected_and_unused_methods fixture. After each run, the script checked that all 150 classes were left unchanged.

Command, run in each checkout with the same vendor/ and the same bench file:

php -d memory_limit=512M -d opcache.enable_cli=0 bench/remove_unused_private_method_bench.php

Environment: PHP 8.4.27 CLI (NTS), Docker limited to 1 CPU / 768 MiB, AMD EPYC-Rome VPS. Fresh process per run, base/head order alternated.

The rule file, fixture and bench script in a7b0485 are identical to the current head after the rebase. A rerun on current main (e60b5ef) vs head (5dd7075) gave the same ratio: 6.86 s -> 1.66 s CPU (median of 5).

This measures direct rule calls on classes where the early name checks apply. It is not an end-to-end project runtime.

@alfredbez
alfredbez force-pushed the fix/rector-perf-remove-unused-private-method-guard branch from 407339b to 3cea0fa Compare October 23, 2026 23:38
@TomasVotruba

TomasVotruba commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Reorder is behavior-neutral and the win looks real (name-list checks now gate isClassMethodUsed(), which traverses the whole class AST, plus scope fetched once per class instead of per method).

Could you add a quick before/after number though? This is the only one of the three perf PRs touching the hot traversal path, yet it ships no bench. A small bench/*.php micro-script (LazyContainerFactory + TestingParser, getrusage CPU, warmup excluded) on both branches would lock it in - same shape as the one in #8801.

@alfredbez
alfredbez force-pushed the fix/rector-perf-remove-unused-private-method-guard branch from 3cea0fa to a7b0485 Compare October 24, 2026 09:55
@TomasVotruba

Copy link
Copy Markdown
Member

Could you update PR descripotion with before/after bench time?

@alfredbez
alfredbez force-pushed the fix/rector-perf-remove-unused-private-method-guard branch from a7b0485 to fea1eb6 Compare October 24, 2026 23:52
@alfredbez
alfredbez force-pushed the fix/rector-perf-remove-unused-private-method-guard branch from fea1eb6 to 5dd7075 Compare October 25, 2026 06:26
@alfredbez

Copy link
Copy Markdown
Contributor Author

Could you update PR descripotion with before/after bench time?

Done ✅

@TomasVotruba

Copy link
Copy Markdown
Member

Thanks 👍

@TomasVotruba
TomasVotruba merged commit 07c2599 into rectorphp:main Oct 25, 2026
49 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants