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 23 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
115 changes: 99 additions & 16 deletions src/TestFramework/Coverage/JUnit/JUnitTestCaseSorter.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,53 +36,134 @@
namespace Infection\TestFramework\Coverage\JUnit;

use function array_key_exists;
use function count;
use function current;
use Infection\AbstractTestFramework\Coverage\TestLocation;
use function Safe\ksort;
use function Safe\usort;

/**
* @internal
*/
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);

/** @var string $filePath */
$filePath = $testLocation->getFilePath();

if ($filePath !== null) {
yield $filePath;
}

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(
self::bucketSort($uniqueTestLocations)
);
}

/**
* Sorts tests to run the fastest first. Exposed for benchmarking purposes.
*
* @param TestLocation[] $uniqueTestLocations
*
* @return iterable<TestLocation>
*/
public static function bucketSort(array $uniqueTestLocations): iterable
theofidry marked this conversation as resolved.
Show resolved Hide resolved
{
// 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) {
foreach ($bucket as $value) {
sanmai marked this conversation as resolved.
Show resolved Hide resolved
// not `yield from` here because it'll break order in PHP 7.3
yield $value;
}
}
}

/**
* @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 +179,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()
if (!isset($usedFileNames[$filePath])) {
theofidry marked this conversation as resolved.
Show resolved Hide resolved
$uniqueTests[] = $testLocation;
$usedFileNames[$filePath] = true;
}
Expand Down