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

Benchmark for JUnitTestCaseSorter::uniqueByTestFile #1177

Merged
merged 38 commits into from
Mar 24, 2020
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
8d9c455
Sample of JUnit test timing from a real project
sanmai Mar 17, 2020
a75c87c
Remove import
sanmai Mar 17, 2020
a08af4d
Implement bucket sort
sanmai Mar 17, 2020
0ca0461
Merge branch 'master' into pr/2020-03/test-timings
sanmai Mar 18, 2020
96f1aea
Improve bucket-sort
sanmai Mar 18, 2020
9eff4ed
Benchmark new sorter
sanmai Mar 18, 2020
1c6e622
CS
sanmai Mar 18, 2020
d177238
PHPStan fixes
sanmai Mar 18, 2020
9239bbf
Merge branch 'master' into pr/2020-03/test-timings
sanmai Mar 19, 2020
ede73cb
Merge branch 'pr/2020-03/test-timings' of github.com:sanmai/infection…
sanmai Mar 18, 2020
bda8f2e
Update to isOrderConstraintsValid
sanmai Mar 19, 2020
dda7f1f
Update MutationConfigBuilderTest
sanmai Mar 19, 2020
a91f031
Use dual approach
sanmai Mar 19, 2020
d95c601
Count once, comment about yield from
sanmai Mar 19, 2020
e41a179
Remove small section sort
sanmai Mar 19, 2020
cd4ceaf
Random tests are brittle, remove
sanmai Mar 19, 2020
fcc70a3
Update src/TestFramework/Coverage/JUnit/JUnitTestCaseSorter.php
sanmai Mar 20, 2020
e96f040
Address review comments
sanmai Mar 19, 2020
e023e9e
Merge branch 'master' into pr/2020-03/test-timings
sanmai Mar 20, 2020
56dfa1b
Compare down to third digit only
sanmai Mar 19, 2020
a6cd4ac
Merge branch 'master' into pr/2020-03/test-timings
sanmai Mar 20, 2020
1b942c8
Merge branch 'master' into pr/2020-03/test-timings
sanmai Mar 20, 2020
10532f0
Skip benchmarks under a debugger
sanmai Mar 19, 2020
d2e6c7a
Update test_it_returns_first_file_name_if_there_is_only_one
sanmai Mar 22, 2020
0a4eccd
Improve test_it_returns_unique_and_sorted_by_time_test_cases
sanmai Mar 22, 2020
d03d706
Address review comments
sanmai Mar 22, 2020
64d82dd
Factor out bucket sort into TestLocationBucketSorter
sanmai Mar 22, 2020
7726cfc
Update TestLocationBucketSorterTest
sanmai Mar 22, 2020
3153574
Merge branch 'pr/2020-03/test-timings' of github.com:sanmai/infection…
sanmai Mar 22, 2020
4a8f119
Move initial array to a constant
sanmai Mar 22, 2020
6f8e0e2
Update TestLocationBucketSorterTest
sanmai Mar 22, 2020
356d7bb
Merge branch 'master' into pr/2020-03/test-timings
sanmai Mar 23, 2020
7a606f2
Use yield from
sanmai Mar 22, 2020
c3db77a
Tweak JUnitTestCaseSorter (#1187)
sanmai Mar 23, 2020
4bd83ad
Merge branch 'master' into pr/2020-03/test-timings
sanmai Mar 23, 2020
ce154c6
Update tests/phpunit/TestFramework/Coverage/JUnit/JUnitTestCaseSorter…
sanmai Mar 23, 2020
a4e6938
Merge branch 'pr/2020-03/test-timings' of github.com:sanmai/infection…
sanmai Mar 23, 2020
4f53178
CS
sanmai Mar 22, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 0 additions & 4 deletions devTools/phpstan-src.neon
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,6 @@ parameters:
path: '../src/Container.php'
message: '#^Parameter \#1 \$processHandler of class Infection\\Process\\Runner\\ParallelProcessRunner constructor expects Closure\(Infection\\Process\\Runner\\ProcessBearer\)\: void\, Closure\(Infection\\Process\\MutantProcess\): void given\.$#'
count: 1
-
path: '../src/TestFramework/Coverage/JUnit/JUnitTestCaseSorter.php'
message: '#array_key_exists expects int\|string, string\|null given#'
count: 1
-
path: '../src/FileSystem/DummyFileSystem.php'
message: '#Infection\\FileSystem\\DummyFileSystem#'
Expand Down
1 change: 1 addition & 0 deletions src/Process/Runner/MutationTestingRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ public function run(iterable $mutations, string $testFrameworkExtraOptions): voi
return $this->mutantFactory->create($mutation);
})
->filter(function (Mutant $mutant) {
// It's a proxy call to Mutation, can be done one stage up
if ($mutant->isCoveredByTest()) {
return true;
}
Expand Down
76 changes: 60 additions & 16 deletions src/TestFramework/Coverage/JUnit/JUnitTestCaseSorter.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
namespace Infection\TestFramework\Coverage\JUnit;

use function array_key_exists;
use function count;
use function current;
use Infection\AbstractTestFramework\Coverage\TestLocation;
use function Safe\usort;
Expand All @@ -45,44 +46,85 @@
*/
final class JUnitTestCaseSorter
{
/**
* Expected average number of buckets. Exposed for testing purposes.
*
* @var int[]
*/
public const BUCKETS_COUNT = 25;

/**
* For 25 buckets QS becomes theoretically less efficient on average at and after 15 elements.
* Exposed for testing purposes.
*/
public const USE_BUCKET_SORT_AFTER = 15;

/**
* @param TestLocation[] $tests
*
* @return string[]
* @return iterable<string>
*/
public function getUniqueSortedFileNames(array $tests): iterable
{
$uniqueTestLocations = $this->uniqueByTestFile($tests);

if (count($uniqueTestLocations) === 1) {
$numberOfTestLocation = count($uniqueTestLocations);

if ($numberOfTestLocation === 1) {
// Around 5% speed up compared to when without this optimization.
/** @var TestLocation $testLocation */
$testLocation = current($uniqueTestLocations);

$filePath = $testLocation->getFilePath();
/*
* TestLocation gets its file path and timings from TestFileTimeData.
* Path for TestFileTimeData is not optional. It is never a null.
* Therefore we don't need to make any type checks here.
*/

if ($filePath !== null) {
yield $filePath;
}
/** @var string $filePath */
$filePath = $testLocation->getFilePath();

return;
return [$filePath];
}

/*
* We need to sort tests to run the fastest first.
*
* Two tests per file are also very frequent. Yet it doesn't make sense
* to sort them by hand: usort does that just as good.
*/

// sort tests to run the fastest first
usort(
$uniqueTestLocations,
static function (TestLocation $a, TestLocation $b) {
return $a->getExecutionTime() <=> $b->getExecutionTime();
}
if ($numberOfTestLocation < self::USE_BUCKET_SORT_AFTER) {
usort(
$uniqueTestLocations,
static function (TestLocation $a, TestLocation $b) {
return $a->getExecutionTime() <=> $b->getExecutionTime();
}
);

return self::sortedLocationsGenerator($uniqueTestLocations);
}

/*
* For large number of tests use a more efficient algorithm.
*/
return self::sortedLocationsGenerator(
TestLocationBucketSorter::bucketSort($uniqueTestLocations)
);
}

/**
* @param iterable<TestLocation> $sortedTestLocations
*
* @return iterable<string>
*/
private static function sortedLocationsGenerator(iterable $sortedTestLocations): iterable
{
foreach ($sortedTestLocations as $testLocation) {
/** @var string $filePath */
$filePath = $testLocation->getFilePath();

foreach ($uniqueTestLocations as $testLocation) {
yield $testLocation->getFilePath();
yield $filePath;
}
}

Expand All @@ -98,9 +140,11 @@ private function uniqueByTestFile(array $testLocations): array
$uniqueTests = [];

foreach ($testLocations as $testLocation) {
/** @var string $filePath */
theofidry marked this conversation as resolved.
Show resolved Hide resolved
$filePath = $testLocation->getFilePath();

if (!array_key_exists($filePath, $usedFileNames)) {
// isset() is 20% faster than array_key_exists() as of PHP 7.3
if (!isset($usedFileNames[$filePath])) {
theofidry marked this conversation as resolved.
Show resolved Hide resolved
$uniqueTests[] = $testLocation;
$usedFileNames[$filePath] = true;
}
Expand Down
90 changes: 90 additions & 0 deletions src/TestFramework/Coverage/JUnit/TestLocationBucketSorter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php
/**
* This code is licensed under the BSD 3-Clause License.
*
* Copyright (c) 2017, Maks Rafalko
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/

declare(strict_types=1);

namespace Infection\TestFramework\Coverage\JUnit;

use Infection\AbstractTestFramework\Coverage\TestLocation;
use function Safe\ksort;

/**
* @internal
*/
final class TestLocationBucketSorter
{
private function __construct()
{
}

/**
* Sorts tests to run the fastest first. Exposed for benchmarking purposes.
*
* @param TestLocation[] $uniqueTestLocations
*
* @return iterable<TestLocation>
*/
public static function bucketSort(array $uniqueTestLocations): iterable
{
// Pre-sort first buckets, optimistically assuming that
// most projects won't have tests longer than a second
$buckets = [
0 => [],
1 => [],
2 => [],
3 => [],
4 => [],
5 => [],
6 => [],
7 => [],
];

foreach ($uniqueTestLocations as $location) {
// Quick drop off lower bits, reducing precision to 8th of a second
$msTime = $location->getExecutionTime() * 1024 >> 7; // * 1024 / 128

// For anything above 4 seconds reduce precision to 4 seconds
if ($msTime > 32) {
$msTime = $msTime >> 5 << 5; // 7 + 5 = 12 bits
}

$buckets[$msTime][] = $location;
}

ksort($buckets);

foreach ($buckets as $bucket) {
yield from $bucket;
}
}
}