Skip to content

Commit f31eff7

Browse files
committed
✨ add PHPBench to run benchmarks
1 parent e633382 commit f31eff7

19 files changed

+1034
-366
lines changed

.phan/config.php

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
// Thus, both first-party and third-party code being used by
2626
// your application should be included in this list.
2727
'directory_list' => [
28+
'benchmark',
2829
'examples',
2930
'src',
3031
'tests',

benchmark/BenchmarkAbstract.php

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
/**
3+
* Class BenchmarkAbstract
4+
*
5+
* @created 23.04.2024
6+
* @author smiley <[email protected]>
7+
* @copyright 2024 smiley
8+
* @license MIT
9+
*/
10+
declare(strict_types=1);
11+
12+
namespace chillerlan\QRCodeBenchmark;
13+
14+
use chillerlan\QRCode\{QRCode, QROptions};
15+
use chillerlan\QRCode\Common\{EccLevel, Mode, Version};
16+
use chillerlan\QRCode\Data\QRMatrix;
17+
use chillerlan\QRCodeTest\QRMaxLengthTrait;
18+
use PhpBench\Attributes\{Iterations, ParamProviders, Revs, Warmup};
19+
use Generator, RuntimeException;
20+
use function extension_loaded, is_dir, mb_substr, mkdir, sprintf, str_repeat, str_replace;
21+
22+
/**
23+
* The abstract benchmark with common methods
24+
*/
25+
#[Iterations(3)]
26+
#[Warmup(3)]
27+
#[Revs(100)]
28+
#[ParamProviders(['versionProvider', 'eccLevelProvider', 'dataModeProvider'])]
29+
abstract class BenchmarkAbstract{
30+
use QRMaxLengthTrait;
31+
32+
protected const BUILDDIR = __DIR__.'/../.build/phpbench/';
33+
protected const ECC_LEVELS = [EccLevel::L, EccLevel::M, EccLevel::Q, EccLevel::H];
34+
protected const DATAMODES = Mode::INTERFACES;
35+
36+
protected array $dataModeData;
37+
protected string $testData;
38+
protected QROptions $options;
39+
protected QRMatrix $matrix;
40+
41+
// properties from data providers
42+
protected Version $version;
43+
protected EccLevel $eccLevel;
44+
protected int $mode;
45+
protected string $modeFQCN;
46+
47+
/**
48+
*
49+
*/
50+
public function __construct(){
51+
52+
foreach(['gd', 'imagick'] as $ext){
53+
if(!extension_loaded($ext)){
54+
throw new RuntimeException(sprintf('ext-%s not loaded', $ext));
55+
}
56+
}
57+
58+
if(!is_dir(self::BUILDDIR)){
59+
mkdir(directory: self::BUILDDIR, recursive: true);
60+
}
61+
62+
$this->dataModeData = $this->generateDataModeData();
63+
}
64+
65+
/**
66+
* Generates test data strings for each mode
67+
*/
68+
protected function generateDataModeData():array{
69+
return [
70+
Mode::NUMBER => str_repeat('0123456789', 750),
71+
Mode::ALPHANUM => str_repeat('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 $%*+-./:', 100),
72+
Mode::KANJI => str_repeat('漂う花の香り', 350),
73+
Mode::HANZI => str_repeat('无可奈何燃花作香', 250),
74+
Mode::BYTE => str_repeat('https://www.youtube.com/watch?v=dQw4w9WgXcQ ', 100),
75+
];
76+
}
77+
78+
/**
79+
* Generates a test max-length data string for the given version, ecc level and data mode
80+
*/
81+
protected function getData(Version $version, EccLevel $eccLevel, int $mode):string{
82+
$maxLength = self::getMaxLengthForMode($mode, $version, $eccLevel);
83+
84+
if($mode === Mode::KANJI || $mode === Mode::HANZI){
85+
return mb_substr($this->dataModeData[$mode], 0, $maxLength);
86+
}
87+
88+
return mb_substr($this->dataModeData[$mode], 0, $maxLength, '8bit');
89+
}
90+
91+
/**
92+
* Initializes a QROptions instance and assigns it to its temp property
93+
*/
94+
protected function initQROptions(array $options):void{
95+
$this->options = new QROptions($options);
96+
}
97+
98+
/**
99+
* Initializes a QRMatrix instance and assigns it to its temp property
100+
*/
101+
public function initMatrix():void{
102+
$this->matrix = (new QRCode($this->options))
103+
->addByteSegment($this->testData)
104+
->getQRMatrix()
105+
;
106+
}
107+
108+
/**
109+
* Generates a test data string and assigns it to its temp property
110+
*/
111+
public function generateTestData():void{
112+
$this->testData = $this->getData($this->version, $this->eccLevel, $this->mode);
113+
}
114+
115+
/**
116+
* Assigns the parameter array from the providers to properties and enforces the types
117+
*/
118+
public function assignParams(array $params):void{
119+
foreach($params as $k => $v){
120+
$this->{$k} = $v;
121+
}
122+
}
123+
124+
public function versionProvider():Generator{
125+
for($v = 1; $v <= 40; $v++){
126+
$version = new Version($v);
127+
128+
yield (string)$version => ['version' => $version];
129+
}
130+
}
131+
132+
public function eccLevelProvider():Generator{
133+
foreach(static::ECC_LEVELS as $ecc){
134+
$eccLevel = new EccLevel($ecc);
135+
136+
yield (string)$eccLevel => ['eccLevel' => $eccLevel];
137+
}
138+
}
139+
140+
public function dataModeProvider():Generator{
141+
foreach(static::DATAMODES as $mode => $modeFQCN){
142+
$name = str_replace('chillerlan\\QRCode\\Data\\', '', $modeFQCN);
143+
144+
yield $name => ['mode' => $mode, 'modeFQCN' => $modeFQCN];
145+
}
146+
}
147+
148+
}

benchmark/DecoderBenchmark.php

+93
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
/**
3+
* Class DecoderBenchmark
4+
*
5+
* @created 26.04.2024
6+
* @author smiley <[email protected]>
7+
* @copyright 2024 smiley
8+
* @license MIT
9+
*/
10+
declare(strict_types=1);
11+
12+
namespace chillerlan\QRCodeBenchmark;
13+
14+
use chillerlan\QRCode\Common\{GDLuminanceSource, IMagickLuminanceSource, Mode};
15+
use chillerlan\QRCode\Data\Byte;
16+
use chillerlan\QRCode\Decoder\{Decoder, DecoderResult};
17+
use chillerlan\QRCode\Output\QRGdImagePNG;
18+
use chillerlan\QRCode\QRCodeException;
19+
use PhpBench\Attributes\{AfterMethods, BeforeMethods, Subject};
20+
use RuntimeException;
21+
22+
/**
23+
* Tests the performance of the QR Code reader/decoder
24+
*/
25+
final class DecoderBenchmark extends BenchmarkAbstract{
26+
27+
protected const DATAMODES = [Mode::BYTE => Byte::class];
28+
29+
private string $imageBlob;
30+
private DecoderResult $result;
31+
32+
public function initOptions():void{
33+
34+
$options = [
35+
'version' => $this->version->getVersionNumber(),
36+
'eccLevel' => $this->eccLevel->getLevel(),
37+
'scale' => 2,
38+
'imageTransparent' => false,
39+
'outputBase64' => false,
40+
];
41+
42+
$this->initQROptions($options);
43+
}
44+
45+
public function generateImageBlob():void{
46+
$this->imageBlob = (new QRGdImagePNG($this->options, $this->matrix))->dump();
47+
}
48+
49+
public function checkReaderResult():void{
50+
if($this->result->data !== $this->testData){
51+
throw new RuntimeException('invalid reader result');
52+
}
53+
}
54+
55+
/**
56+
* Tests the performance of the GD reader
57+
*/
58+
#[Subject]
59+
#[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initMatrix', 'generateImageBlob'])]
60+
#[AfterMethods(['checkReaderResult'])]
61+
public function GDLuminanceSource():void{
62+
63+
// in rare cases the reader will be unable to detect and throw,
64+
// but we don't want the performance test to yell about it
65+
// @see QRCodeReaderTestAbstract::testReadData()
66+
try{
67+
$this->result = (new Decoder($this->options))
68+
->decode(GDLuminanceSource::fromBlob($this->imageBlob, $this->options));
69+
}
70+
catch(QRCodeException){
71+
// noop
72+
}
73+
}
74+
75+
/**
76+
* Tests the performance of the ImageMagick reader
77+
*/
78+
#[Subject]
79+
#[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initMatrix', 'generateImageBlob'])]
80+
#[AfterMethods(['checkReaderResult'])]
81+
public function IMagickLuminanceSource():void{
82+
$this->options->readerUseImagickIfAvailable = true;
83+
84+
try{
85+
$this->result = (new Decoder($this->options))
86+
->decode(IMagickLuminanceSource::fromBlob($this->imageBlob, $this->options));
87+
}
88+
catch(QRCodeException){
89+
// noop
90+
}
91+
}
92+
93+
}

benchmark/MaskPatternBenchmark.php

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
/**
3+
* Class MaskPatternBenchmark
4+
*
5+
* @created 23.04.2024
6+
* @author smiley <[email protected]>
7+
* @copyright 2024 smiley
8+
* @license MIT
9+
*/
10+
declare(strict_types=1);
11+
12+
namespace chillerlan\QRCodeBenchmark;
13+
14+
use chillerlan\QRCode\Common\{MaskPattern, Mode};
15+
use chillerlan\QRCode\Data\Byte;
16+
use PhpBench\Attributes\{BeforeMethods, Subject};
17+
18+
/**
19+
* Tests the performance of the mask pattern penalty testing
20+
*/
21+
final class MaskPatternBenchmark extends BenchmarkAbstract{
22+
23+
protected const DATAMODES = [Mode::BYTE => Byte::class];
24+
25+
public function initOptions():void{
26+
27+
$options = [
28+
'version' => $this->version->getVersionNumber(),
29+
'eccLevel' => $this->eccLevel->getLevel(),
30+
];
31+
32+
$this->initQROptions($options);
33+
}
34+
35+
#[Subject]
36+
#[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initMatrix'])]
37+
public function getBestPattern():void{
38+
MaskPattern::getBestPattern($this->matrix);
39+
}
40+
41+
}

benchmark/OutputBenchmark.php

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
/**
3+
* Class OutputBenchmark
4+
*
5+
* @created 23.04.2024
6+
* @author smiley <[email protected]>
7+
* @copyright 2024 smiley
8+
* @license MIT
9+
*/
10+
declare(strict_types=1);
11+
12+
namespace chillerlan\QRCodeBenchmark;
13+
14+
use chillerlan\QRCode\Common\{EccLevel, Mode};
15+
use chillerlan\QRCode\Data\Byte;
16+
use chillerlan\QRCode\Output\{
17+
QREps, QRFpdf, QRGdImageJPEG, QRGdImagePNG, QRGdImageWEBP, QRImagick, QRMarkupSVG, QRStringJSON
18+
};
19+
use PhpBench\Attributes\{BeforeMethods, Subject};
20+
21+
/**
22+
* Tests the performance of the built-in output classes
23+
*/
24+
#[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initMatrix'])]
25+
final class OutputBenchmark extends BenchmarkAbstract{
26+
27+
protected const ECC_LEVELS = [EccLevel::H];
28+
protected const DATAMODES = [Mode::BYTE => Byte::class];
29+
30+
public function initOptions():void{
31+
32+
$options = [
33+
'version' => $this->version->getVersionNumber(),
34+
'eccLevel' => $this->eccLevel->getLevel(),
35+
'connectPaths' => true,
36+
'drawLightModules' => true,
37+
'drawCircularModules' => true,
38+
'gdImageUseUpscale' => false, // set to false to allow proper comparison
39+
];
40+
41+
$this->initQROptions($options);
42+
}
43+
44+
#[Subject]
45+
public function QREps():void{
46+
(new QREps($this->options, $this->matrix))->dump();
47+
}
48+
49+
#[Subject]
50+
public function QRFpdf():void{
51+
(new QRFpdf($this->options, $this->matrix))->dump();
52+
}
53+
54+
/**
55+
* for some reason imageavif() is extremely slow, ~50x slower than imagepng()
56+
*/
57+
# #[Subject]
58+
# public function QRGdImageAVIF():void{
59+
# (new QRGdImageAVIF($this->options, $this->matrix))->dump();
60+
# }
61+
62+
#[Subject]
63+
public function QRGdImageJPEG():void{
64+
(new QRGdImageJPEG($this->options, $this->matrix))->dump();
65+
}
66+
67+
#[Subject]
68+
public function QRGdImagePNG():void{
69+
(new QRGdImagePNG($this->options, $this->matrix))->dump();
70+
}
71+
72+
#[Subject]
73+
public function QRGdImageWEBP():void{
74+
(new QRGdImageWEBP($this->options, $this->matrix))->dump();
75+
}
76+
77+
#[Subject]
78+
public function QRImagick():void{
79+
(new QRImagick($this->options, $this->matrix))->dump();
80+
}
81+
82+
#[Subject]
83+
public function QRMarkupSVG():void{
84+
(new QRMarkupSVG($this->options, $this->matrix))->dump();
85+
}
86+
87+
#[Subject]
88+
public function QRStringJSON():void{
89+
(new QRStringJSON($this->options, $this->matrix))->dump();
90+
}
91+
92+
}

0 commit comments

Comments
 (0)