Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC: Speed optimize class-loading for levels tests #2916

Merged
merged 17 commits into from Feb 18, 2024

Conversation

staabm
Copy link
Contributor

@staabm staabm commented Feb 14, 2024

level tests are the slowest part of the CI process and take a considerable amount of time.
they usually take arround something like 10 minutes

locally I have reduced rule level tests to a small subset to easy reproducing the problem.
my reduced set of rule-level tests before this PR run like

Warning:       No code coverage driver available

.                                                                   1 / 1 (100%)

Time: 01:12.819, Memory: 32.00 MB

after the change in autoload rules it runs like

Warning:       No code coverage driver available

.                                                                   1 / 1 (100%)

Time: 00:46.306, Memory: 24.00 MB

which is ~37% faster.

lets see wheter this speedup reproduces in CI and the change itself is acceptable


side-note: the PR as is currently yields to 2 notices when running phpunit, which obviously should be fixed
fixed by 11ffaa8

$ time vendor/bin/phpunit tests/PHPStan/Generics/GenericsIntegrationTest.php --group levels
PHP Warning:  Class "ReturnTypes\Foo" not found in C:\dvl\Workspace\phpstan-src-staabm\tests\phpstan-bootstrap.php on line 8

Warning: Class "ReturnTypes\Foo" not found in C:\dvl\Workspace\phpstan-src-staabm\tests\phpstan-bootstrap.php on line 8
PHP Warning:  Class "TestAccessProperties\FooAccessProperties" not found in C:\dvl\Workspace\phpstan-src-staabm\tests\phpstan-bootstrap.php on line 9

Warning: Class "TestAccessProperties\FooAccessProperties" not found in C:\dvl\Workspace\phpstan-src-staabm\tests\phpstan-bootstrap.php on line 9

@staabm
Copy link
Contributor Author

staabm commented Feb 14, 2024

In CI it seems we save ~2 minutes with this PR in Tests / Levels tests (reduced from previously ~10 minutes).

the approach of this PR is faster, because ComposerJsonAndInstalledJsonSourceLocatorMaker analyses PHPStan composer.json and needs to verify/check sha1 checksums for all files configured in composer autoloading. In RuleLevelTests we trigger a lot of phpstan processes which all have to re-verify the hash-sums of all files and therefore it gets slow.

With every autoloadable symbol added via a new test to PHPStan the rule-level test gets a tiny bit slower.

"classmap": [
- "tests/e2e",
- "tests/PHPStan"
+ "tests/PHPStan/Rules/Methods/data/returnTypes.php",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the intention, but I don't love it. It's going to be really hard to maintain this list and debug problems as time goes on.

I'd be more interested in parallelizing running the tests. Basically each "topic" in LevelsTestCase could have its own GitHub Actions job.

It's not exactly the same but the idea is here: https://rias.be/blog/running-phpunit-tests-in-parallel-using-github-actions

Copy link
Contributor Author

@staabm staabm Feb 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have adjusted the PR. It now filters the existing tests based on class-name and builds a dynamic matrix with it.

this means we run now 40 tests in parallel (which each takes ~1m 15s) instead of one large job which runs ~10 minutes

grafik

the tests.yml github action workflow for this PR finished in 4m 15s, while it takes ~10m 30s before this PR 🚀

Copy link
Contributor

@bendavies bendavies Feb 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the intention, but I don't love it. It's going to be really hard to maintain this list and debug problems as time goes on.

I'd be more interested in parallelizing running the tests. Basically each "topic" in LevelsTestCase could have its own GitHub Actions job.

It's not exactly the same but the idea is here: rias.be/blog/running-phpunit-tests-in-parallel-using-github-actions

This is very similar to what i did in psalm years ago:
vimeo/psalm#4985

But in psalm, each job also runs test in parallel.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bendavies the tests jobs - even before this PR - are already highly parallelized - within the github action jobs and also across several jobs.

This PR splits up one very slow part of the pre-existing tests into even more jobs because these were particular slow

Copy link
Contributor

@bendavies bendavies Feb 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 apologies.
i couldn't see (still can't) where tests are split across jobs.

Copy link
Contributor Author

@staabm staabm Feb 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apologies

no worries.

i couldn't see (still can't) where tests are split across jobs.

see the screen in my above comment. there you can see the "levels test" matrix, with several jobs.
if you compare to other existing PR, this step is just a single job.

the PR implements this by inspecting the test-suite level-tests test-cases. Because of the used dataprovider, this means the testLevels method represents several test-cases. each of these test-cases is run in a separate github action job, because I feed this list of test-cases as a json array into the "test-levels" github action job matrix.

@@ -0,0 +1,31 @@
<?php

$testList = shell_exec('php vendor/bin/phpunit --list-tests');
Copy link
Contributor Author

@staabm staabm Feb 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

currently the --list-tests phpunit feature does not allow filtering by test-groups.

I have opened a feature request about that, so we may can get rid/simplify this script in the future.

sebastianbergmann/phpunit#5702

@staabm staabm marked this pull request as draft February 16, 2024 14:05
@staabm staabm marked this pull request as ready for review February 16, 2024 14:13
@phpstan-bot
Copy link
Collaborator

This pull request has been marked as ready for review.

Copy link
Member

@ondrejmirtes ondrejmirtes left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really love this! Just found a few minor things :)


if (
!str_contains($cleanedLine, 'PHPStan\Generics\GenericsIntegrationTest') &&
!str_contains($cleanedLine, 'PHPStan\Levels\LevelsIntegrationTest')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should check the @group inside the docblock instead of hardcoding these two classes.

Copy link
Contributor Author

@staabm staabm Feb 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to make this work based on the phpdoc, I would have needed to reflect on classes and read all class-comments.

instead I updated phpunit to 10.x for the matrix job and used --list-tests-xml which returns a xml containing group information. its a option which does not exist in phpunit 9.x

@@ -0,0 +1,33 @@
<?php
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

declare strict types please

uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
php-version: "8.1"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do 8.3 here :)

@@ -121,7 +155,7 @@ jobs:
run: "composer install --no-interaction --no-progress"

- name: "Tests"
run: "make tests-levels"
run: "php vendor/bin/phpunit --filter '${{ matrix.test-filter }}' --group levels"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just did some local testing and in this format PHPUnit will still execute all data providers from all tests. It would be good to also add the file in which the filtered test is. I think we can get even faster that way.

See the comparison:

time vendor/bin/phpunit --filter 'PHPStan\\Type\\UnionTypeTest::testSorting'
15.7s

time vendor/bin/phpunit tests/PHPStan/Type/UnionTypeTest.php --filter 'PHPStan\\Type\\UnionTypeTest::testSorting'
1.1s

Copy link
Contributor Author

@staabm staabm Feb 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, it got even faster now :)

grafik


$filter = str_replace('\\', '\\\\', $testCaseName);

$testFilters[] = sprintf("%s --filter '%s'", $fileName, $filter);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is doubling backslashes needed? I’d say that instead of that we need escapeshellarg() for both fileName and filter.

Copy link
Contributor Author

@staabm staabm Feb 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Running the plain command on the CLI only works only with double backslashes (in the filter arg)

See your command in #2916 (comment)

Copy link
Contributor Author

@staabm staabm Feb 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we can add escapeshellarg(), but we still need this manual escaping

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets give it a try and we will see

Copy link
Contributor Author

@staabm staabm Feb 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/phpstan/phpstan-src/actions/runs/7944377066/job/21689826256 shows that it doesn't work when we only use escapeshellarg (no tests executed).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is that? I noticed it doesn't work but I don't know what eats the characters, whether it's bash or PHP. Doubling the backslashes seems very peculiar to me, maybe there are other things we need to fix too but we're currently unaware of that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phpunit examples in docs also mention double backslashes

https://docs.phpunit.de/en/9.6/textui.html#textui-examples-filter-patterns

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its interpreted as regex. Let mit try preg_quote()

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know. It's probably a bash thing.

Copy link
Contributor Author

@staabm staabm Feb 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

preg_quote would escape the following characters: . \ + * ? [ ^ ] $ ( ) { } = ! < > | : - #

the only one of these which can show up in a class/method name is \. I think we can use preg_quote if you prefer but it should be enough to escape the backslash IMO

edit: I had an idea

@ondrejmirtes ondrejmirtes merged commit 938321c into phpstan:1.10.x Feb 18, 2024
471 checks passed
@ondrejmirtes
Copy link
Member

Thank you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
4 participants