diff --git a/.phan/config.php b/.phan/config.php index cb6554b66..82154d3ff 100644 --- a/.phan/config.php +++ b/.phan/config.php @@ -25,6 +25,7 @@ return [ // Thus, both first-party and third-party code being used by // your application should be included in this list. 'directory_list' => [ + 'benchmark', 'examples', 'src', 'tests', diff --git a/benchmark/BenchmarkAbstract.php b/benchmark/BenchmarkAbstract.php new file mode 100644 index 000000000..5cfebdb6d --- /dev/null +++ b/benchmark/BenchmarkAbstract.php @@ -0,0 +1,148 @@ + + * @copyright 2024 smiley + * @license MIT + */ +declare(strict_types=1); + +namespace chillerlan\QRCodeBenchmark; + +use chillerlan\QRCode\{QRCode, QROptions}; +use chillerlan\QRCode\Common\{EccLevel, Mode, Version}; +use chillerlan\QRCode\Data\QRMatrix; +use chillerlan\QRCodeTest\QRMaxLengthTrait; +use PhpBench\Attributes\{Iterations, ParamProviders, Revs, Warmup}; +use Generator, RuntimeException; +use function extension_loaded, is_dir, mb_substr, mkdir, sprintf, str_repeat, str_replace; + +/** + * The abstract benchmark with common methods + */ +#[Iterations(3)] +#[Warmup(3)] +#[Revs(100)] +#[ParamProviders(['versionProvider', 'eccLevelProvider', 'dataModeProvider'])] +abstract class BenchmarkAbstract{ + use QRMaxLengthTrait; + + protected const BUILDDIR = __DIR__.'/../.build/phpbench/'; + protected const ECC_LEVELS = [EccLevel::L, EccLevel::M, EccLevel::Q, EccLevel::H]; + protected const DATAMODES = Mode::INTERFACES; + + protected array $dataModeData; + protected string $testData; + protected QROptions $options; + protected QRMatrix $matrix; + + // properties from data providers + protected Version $version; + protected EccLevel $eccLevel; + protected int $mode; + protected string $modeFQCN; + + /** + * + */ + public function __construct(){ + + foreach(['gd', 'imagick'] as $ext){ + if(!extension_loaded($ext)){ + throw new RuntimeException(sprintf('ext-%s not loaded', $ext)); + } + } + + if(!is_dir(self::BUILDDIR)){ + mkdir(directory: self::BUILDDIR, recursive: true); + } + + $this->dataModeData = $this->generateDataModeData(); + } + + /** + * Generates test data strings for each mode + */ + protected function generateDataModeData():array{ + return [ + Mode::NUMBER => str_repeat('0123456789', 750), + Mode::ALPHANUM => str_repeat('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 $%*+-./:', 100), + Mode::KANJI => str_repeat('漂う花の香り', 350), + Mode::HANZI => str_repeat('无可奈何燃花作香', 250), + Mode::BYTE => str_repeat('https://www.youtube.com/watch?v=dQw4w9WgXcQ ', 100), + ]; + } + + /** + * Generates a test max-length data string for the given version, ecc level and data mode + */ + protected function getData(Version $version, EccLevel $eccLevel, int $mode):string{ + $maxLength = self::getMaxLengthForMode($mode, $version, $eccLevel); + + if($mode === Mode::KANJI || $mode === Mode::HANZI){ + return mb_substr($this->dataModeData[$mode], 0, $maxLength); + } + + return mb_substr($this->dataModeData[$mode], 0, $maxLength, '8bit'); + } + + /** + * Initializes a QROptions instance and assigns it to its temp property + */ + protected function initQROptions(array $options):void{ + $this->options = new QROptions($options); + } + + /** + * Initializes a QRMatrix instance and assigns it to its temp property + */ + public function initMatrix():void{ + $this->matrix = (new QRCode($this->options)) + ->addByteSegment($this->testData) + ->getQRMatrix() + ; + } + + /** + * Generates a test data string and assigns it to its temp property + */ + public function generateTestData():void{ + $this->testData = $this->getData($this->version, $this->eccLevel, $this->mode); + } + + /** + * Assigns the parameter array from the providers to properties and enforces the types + */ + public function assignParams(array $params):void{ + foreach($params as $k => $v){ + $this->{$k} = $v; + } + } + + public function versionProvider():Generator{ + for($v = 1; $v <= 40; $v++){ + $version = new Version($v); + + yield (string)$version => ['version' => $version]; + } + } + + public function eccLevelProvider():Generator{ + foreach(static::ECC_LEVELS as $ecc){ + $eccLevel = new EccLevel($ecc); + + yield (string)$eccLevel => ['eccLevel' => $eccLevel]; + } + } + + public function dataModeProvider():Generator{ + foreach(static::DATAMODES as $mode => $modeFQCN){ + $name = str_replace('chillerlan\\QRCode\\Data\\', '', $modeFQCN); + + yield $name => ['mode' => $mode, 'modeFQCN' => $modeFQCN]; + } + } + +} diff --git a/benchmark/DecoderBenchmark.php b/benchmark/DecoderBenchmark.php new file mode 100644 index 000000000..44b74ef7d --- /dev/null +++ b/benchmark/DecoderBenchmark.php @@ -0,0 +1,93 @@ + + * @copyright 2024 smiley + * @license MIT + */ +declare(strict_types=1); + +namespace chillerlan\QRCodeBenchmark; + +use chillerlan\QRCode\Common\{GDLuminanceSource, IMagickLuminanceSource, Mode}; +use chillerlan\QRCode\Data\Byte; +use chillerlan\QRCode\Decoder\{Decoder, DecoderResult}; +use chillerlan\QRCode\Output\QRGdImagePNG; +use chillerlan\QRCode\QRCodeException; +use PhpBench\Attributes\{AfterMethods, BeforeMethods, Subject}; +use RuntimeException; + +/** + * Tests the performance of the QR Code reader/decoder + */ +final class DecoderBenchmark extends BenchmarkAbstract{ + + protected const DATAMODES = [Mode::BYTE => Byte::class]; + + private string $imageBlob; + private DecoderResult $result; + + public function initOptions():void{ + + $options = [ + 'version' => $this->version->getVersionNumber(), + 'eccLevel' => $this->eccLevel->getLevel(), + 'scale' => 2, + 'imageTransparent' => false, + 'outputBase64' => false, + ]; + + $this->initQROptions($options); + } + + public function generateImageBlob():void{ + $this->imageBlob = (new QRGdImagePNG($this->options, $this->matrix))->dump(); + } + + public function checkReaderResult():void{ + if($this->result->data !== $this->testData){ + throw new RuntimeException('invalid reader result'); + } + } + + /** + * Tests the performance of the GD reader + */ + #[Subject] + #[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initMatrix', 'generateImageBlob'])] + #[AfterMethods(['checkReaderResult'])] + public function GDLuminanceSource():void{ + + // in rare cases the reader will be unable to detect and throw, + // but we don't want the performance test to yell about it + // @see QRCodeReaderTestAbstract::testReadData() + try{ + $this->result = (new Decoder($this->options)) + ->decode(GDLuminanceSource::fromBlob($this->imageBlob, $this->options)); + } + catch(QRCodeException){ + // noop + } + } + + /** + * Tests the performance of the ImageMagick reader + */ + #[Subject] + #[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initMatrix', 'generateImageBlob'])] + #[AfterMethods(['checkReaderResult'])] + public function IMagickLuminanceSource():void{ + $this->options->readerUseImagickIfAvailable = true; + + try{ + $this->result = (new Decoder($this->options)) + ->decode(IMagickLuminanceSource::fromBlob($this->imageBlob, $this->options)); + } + catch(QRCodeException){ + // noop + } + } + +} diff --git a/benchmark/MaskPatternBenchmark.php b/benchmark/MaskPatternBenchmark.php new file mode 100644 index 000000000..752fb395f --- /dev/null +++ b/benchmark/MaskPatternBenchmark.php @@ -0,0 +1,41 @@ + + * @copyright 2024 smiley + * @license MIT + */ +declare(strict_types=1); + +namespace chillerlan\QRCodeBenchmark; + +use chillerlan\QRCode\Common\{MaskPattern, Mode}; +use chillerlan\QRCode\Data\Byte; +use PhpBench\Attributes\{BeforeMethods, Subject}; + +/** + * Tests the performance of the mask pattern penalty testing + */ +final class MaskPatternBenchmark extends BenchmarkAbstract{ + + protected const DATAMODES = [Mode::BYTE => Byte::class]; + + public function initOptions():void{ + + $options = [ + 'version' => $this->version->getVersionNumber(), + 'eccLevel' => $this->eccLevel->getLevel(), + ]; + + $this->initQROptions($options); + } + + #[Subject] + #[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initMatrix'])] + public function getBestPattern():void{ + MaskPattern::getBestPattern($this->matrix); + } + +} diff --git a/benchmark/OutputBenchmark.php b/benchmark/OutputBenchmark.php new file mode 100644 index 000000000..3b6c883b5 --- /dev/null +++ b/benchmark/OutputBenchmark.php @@ -0,0 +1,92 @@ + + * @copyright 2024 smiley + * @license MIT + */ +declare(strict_types=1); + +namespace chillerlan\QRCodeBenchmark; + +use chillerlan\QRCode\Common\{EccLevel, Mode}; +use chillerlan\QRCode\Data\Byte; +use chillerlan\QRCode\Output\{ + QREps, QRFpdf, QRGdImageJPEG, QRGdImagePNG, QRGdImageWEBP, QRImagick, QRMarkupSVG, QRStringJSON +}; +use PhpBench\Attributes\{BeforeMethods, Subject}; + +/** + * Tests the performance of the built-in output classes + */ +#[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initMatrix'])] +final class OutputBenchmark extends BenchmarkAbstract{ + + protected const ECC_LEVELS = [EccLevel::H]; + protected const DATAMODES = [Mode::BYTE => Byte::class]; + + public function initOptions():void{ + + $options = [ + 'version' => $this->version->getVersionNumber(), + 'eccLevel' => $this->eccLevel->getLevel(), + 'connectPaths' => true, + 'drawLightModules' => true, + 'drawCircularModules' => true, + 'gdImageUseUpscale' => false, // set to false to allow proper comparison + ]; + + $this->initQROptions($options); + } + + #[Subject] + public function QREps():void{ + (new QREps($this->options, $this->matrix))->dump(); + } + + #[Subject] + public function QRFpdf():void{ + (new QRFpdf($this->options, $this->matrix))->dump(); + } + + /** + * for some reason imageavif() is extremely slow, ~50x slower than imagepng() + */ +# #[Subject] +# public function QRGdImageAVIF():void{ +# (new QRGdImageAVIF($this->options, $this->matrix))->dump(); +# } + + #[Subject] + public function QRGdImageJPEG():void{ + (new QRGdImageJPEG($this->options, $this->matrix))->dump(); + } + + #[Subject] + public function QRGdImagePNG():void{ + (new QRGdImagePNG($this->options, $this->matrix))->dump(); + } + + #[Subject] + public function QRGdImageWEBP():void{ + (new QRGdImageWEBP($this->options, $this->matrix))->dump(); + } + + #[Subject] + public function QRImagick():void{ + (new QRImagick($this->options, $this->matrix))->dump(); + } + + #[Subject] + public function QRMarkupSVG():void{ + (new QRMarkupSVG($this->options, $this->matrix))->dump(); + } + + #[Subject] + public function QRStringJSON():void{ + (new QRStringJSON($this->options, $this->matrix))->dump(); + } + +} diff --git a/benchmark/QRCodeBenchmark.php b/benchmark/QRCodeBenchmark.php new file mode 100644 index 000000000..d55023a97 --- /dev/null +++ b/benchmark/QRCodeBenchmark.php @@ -0,0 +1,40 @@ + + * @copyright 2024 smiley + * @license MIT + */ +declare(strict_types=1); + +namespace chillerlan\QRCodeBenchmark; + +use chillerlan\QRCode\Output\QRStringJSON; +use chillerlan\QRCode\QRCode; +use PhpBench\Attributes\{BeforeMethods, Subject}; + +/** + * Tests the overall performance of the QRCode class + */ +final class QRCodeBenchmark extends BenchmarkAbstract{ + + public function initOptions():void{ + + $options = [ + 'outputInterface' => QRStringJSON::class, + 'version' => $this->version->getVersionNumber(), + 'eccLevel' => $this->eccLevel->getLevel(), + ]; + + $this->initQROptions($options); + } + + #[Subject] + #[BeforeMethods(['assignParams', 'generateTestData', 'initOptions'])] + public function render():void{ + (new QRCode($this->options))->addSegment(new $this->modeFQCN($this->testData))->render(); + } + +} diff --git a/benchmark/QRDataBenchmark.php b/benchmark/QRDataBenchmark.php new file mode 100644 index 000000000..158158987 --- /dev/null +++ b/benchmark/QRDataBenchmark.php @@ -0,0 +1,76 @@ + + * @copyright 2024 smiley + * @license MIT + */ +declare(strict_types=1); + +namespace chillerlan\QRCodeBenchmark; + +use chillerlan\QRCode\Common\BitBuffer; +use chillerlan\QRCode\Data\QRData; +use PhpBench\Attributes\{BeforeMethods, Subject}; + +/** + * Tests the QRMatrix write performance + */ +final class QRDataBenchmark extends BenchmarkAbstract{ + + private QRData $qrData; + private BitBuffer $bitBuffer; + + public function initOptions():void{ + + $options = [ + 'version' => $this->version->getVersionNumber(), + 'eccLevel' => $this->eccLevel->getLevel(), + ]; + + $this->initQROptions($options); + } + + public function initQRData():void{ + $this->qrData = new QRData($this->options, [new $this->modeFQCN($this->testData)]); + } + + public function initBitBuffer():void{ + $this->bitBuffer = $this->qrData->getBitBuffer(); + $this->bitBuffer->read(4); // read data mode indicator + } + + /** + * Tests the performance of QRData invovcation, includes QRData::writeBitBuffer() + */ + #[Subject] + #[BeforeMethods(['assignParams', 'generateTestData', 'initOptions'])] + public function invocation():void{ + /** @phan-suppress-next-line PhanNoopNew */ + new QRData($this->options, [new $this->modeFQCN($this->testData)]); + } + + /** + * Tests the performance of QRData::writeMatrix(), includes QRMatrix::writeCodewords() and the ReedSolomonEncoder + */ + #[Subject] + #[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initQRData'])] + public function writeMatrix():void{ + $this->qrData->writeMatrix(); + } + + /** + * Tests the performance of QRDataModeInterface::decodeSegment() + * + * we need to clone the BitBuffer instance here because its internal state is modified during decoding + */ + #[Subject] + #[BeforeMethods(['assignParams', 'generateTestData', 'initOptions', 'initQRData', 'initBitBuffer'])] + public function decodeSegment():void{ + /** @noinspection PhpUndefinedMethodInspection */ + $this->modeFQCN::decodeSegment(clone $this->bitBuffer, $this->version->getVersionNumber()); + } + +} diff --git a/benchmark/benchmark.css b/benchmark/benchmark.css new file mode 100644 index 000000000..b9e2f13e8 --- /dev/null +++ b/benchmark/benchmark.css @@ -0,0 +1,64 @@ +body { + font-size: 16px; + line-height: 1.25em; + font-family: "Verdana", sans-serif; + color: #000; +} + +code{ + font-family: "Consolas", monospace; +} + +table { + border-collapse: collapse; +} + +td{ + padding: 0.25em 0.5em; +} + +thead > tr:nth-child(1) > th:nth-child(1n+2), +thead > tr:nth-child(even) > th:nth-child(4n+2), +tbody > tr > td:nth-child(4n+2) { + border-left: 2px solid #999; +} + +tr:nth-child(odd), +thead > tr > th:nth-child(even), +thead > tr:nth-child(2) > th:first-child { + background: rgba(0, 0, 0, 0.1); +} + +tbody > tr > td:first-child { + background: rgba(0, 0, 0, 0.1); + font-weight: bold; +} + +tbody > tr > td:nth-child(1n+2) { + text-align: right; +} + +thead > tr:nth-child(2) > th:nth-child(4n+2), +tbody > tr > td:nth-child(4n+2) { + background: rgba(0, 255, 0, 0.1); +} + +thead > tr:nth-child(2) > th:nth-child(4n+3), +tbody > tr > td:nth-child(4n+3) { + background: rgba(255, 255, 0, 0.1); +} + +thead > tr:nth-child(2) > th:nth-child(4n+4), +tbody > tr > td:nth-child(4n+4) { + background: rgba(0, 0, 255, 0.1); +} + +thead > tr:nth-child(2) > th:nth-child(4n+5), +tbody > tr > td:nth-child(4n+5) { + background: rgba(255, 0, 0, 0.1); +} + +a.return{ + display: block; + margin: 1em 0; +} diff --git a/benchmark/generate-html.php b/benchmark/generate-html.php new file mode 100644 index 000000000..fe8024993 --- /dev/null +++ b/benchmark/generate-html.php @@ -0,0 +1,138 @@ + + * @copyright 2024 smiley + * @license MIT + * + * @phan-file-suppress PhanTypeArraySuspiciousNullable + */ +declare(strict_types=1); + +namespace chillerlan\QRCodeBenchmark; + +use RuntimeException; +use function array_keys; +use function copy; +use function file_exists; +use function file_get_contents; +use function file_put_contents; +use function htmlspecialchars; +use function implode; +use function is_bool; +use function is_dir; +use function json_decode; +use function mkdir; +use function sprintf; + +require_once __DIR__.'/parse-common.php'; + +if(!file_exists(FILE.'.json')){ + throw new RuntimeException('invalid benchmark report'); +} + +$json = json_decode(file_get_contents(FILE.'.json'), true); + +$htmlHead = ' + + + + + +%s results + +'; + +$htmlFoot = ' +'; + +$html = []; + +// General information/overview + +$env = $json['env']; +$suite = $json['suite']; + +$html['index'][] = sprintf($htmlHead, 'Benchmark'); +$html['index'][] = '

Benchmark results

'; +$html['index'][] = '

Environment

'; +$html['index'][] = ''; +$html['index'][] = ''; +$html['index'][] = ''; + +$html['index'][] = sprintf('', $suite['date'], $suite['time']); +$html['index'][] = sprintf('', $env['uname_os'], $env['uname_version'], $env['uname_machine']); +$html['index'][] = sprintf('', htmlspecialchars($suite['tag'])); + +foreach(['php_version', 'php_ini', 'php_extensions', 'php_xdebug', 'opcache_extension_loaded', 'opcache_enabled'] as $field){ + + // prettify the boolean values + if(is_bool($env[$field])){ + $env[$field] = ($env[$field]) ? '✓' : '✗'; + } + + $html['index'][] = sprintf('', $field, $env[$field]); +} + +$html['index'][] = '
NameValue
date%s %s
environment%s %s, %s
tag%s
%s%s
'; + +// list indiviidual reports +$html['index'][] = '

Reports

'; + +$list = []; + +foreach(array_keys($json['benchmark']) as $benchmark){ + + // add a file & header + $html[$benchmark][] = sprintf($htmlHead, $benchmark); + $html[$benchmark][] = sprintf('

%s

', $benchmark); + $html[$benchmark][] = sprintf('%s', $json['benchmark'][$benchmark]['class']); + + foreach(array_keys($json['benchmark'][$benchmark]['subjects']) as $subject){ + // list item + $list[$benchmark][] = $subject; + + $subj = $json['benchmark'][$benchmark]['subjects'][$subject]; + + $html[$benchmark][] = sprintf('

%1$s

', $subject); + $html[$benchmark][] = sprintf('
Revs: %s, Iterations: %s
', $subj['revs'], $subj['iterations']); + $html[$benchmark][] = parseVariants($subj['variants']); + } + + // close document + $html[$benchmark][] = 'back to overview'; + $html[$benchmark][] = $htmlFoot; +} + +// create overview list + +$html['index'][] = ''; + +// close document +$html['index'][] = $htmlFoot; + +if(!is_dir(BUILDDIR.'/html/')){ + mkdir(BUILDDIR.'/html/'); +} + +copy('./benchmark.css', BUILDDIR.'/html/benchmark.css'); + +foreach($html as $file => $content){ + file_put_contents(BUILDDIR.'/html/'.$file.'.html', implode("\n", $content)); +} diff --git a/benchmark/generate-markdown.php b/benchmark/generate-markdown.php new file mode 100644 index 000000000..479cb0394 --- /dev/null +++ b/benchmark/generate-markdown.php @@ -0,0 +1,107 @@ + + * @copyright 2024 smiley + * @license MIT + * + * @phan-file-suppress PhanTypeArraySuspiciousNullable + */ +declare(strict_types=1); + +namespace chillerlan\QRCodeBenchmark; + +use RuntimeException; +use function array_keys; +use function file_exists; +use function file_get_contents; +use function file_put_contents; +use function implode; +use function is_bool; +use function is_dir; +use function json_decode; +use function mkdir; +use function sprintf; +use function strtolower; + +require_once __DIR__.'/parse-common.php'; + +if(!file_exists(FILE.'.json')){ + throw new RuntimeException('invalid benchmark report'); +} + +$json = json_decode(file_get_contents(FILE.'.json'), true); +$markdown = []; + +// General information/overview + +$env = $json['env']; +$suite = $json['suite']; + +$markdown['Readme'][] = "# Benchmark results\n"; +$markdown['Readme'][] = "## Environment\n"; +$markdown['Readme'][] = '| Name | Value |'; +$markdown['Readme'][] = '|------|-------|'; +$markdown['Readme'][] = sprintf('| date | %s %s |', $suite['date'], $suite['time']); +$markdown['Readme'][] = sprintf('| environment | %s %s, %s |', $env['uname_os'], $env['uname_version'], $env['uname_machine']); +$markdown['Readme'][] = sprintf('| tag | %s |', $suite['tag']); + + +foreach(['php_version', 'php_ini', 'php_extensions', 'php_xdebug', 'opcache_extension_loaded', 'opcache_enabled'] as $field){ + + // prettify the boolean values + if(is_bool($env[$field])){ + $env[$field] = ($env[$field]) ? '✓' : '✗'; + } + + $markdown['Readme'][] = sprintf('| %s | %s |', $field, $env[$field]); +} + +// list indiviidual reports +$markdown['Readme'][] = ''; +$markdown['Readme'][] = '## Reports'; +$markdown['Readme'][] = ''; + +$list = []; + +foreach(array_keys($json['benchmark']) as $benchmark){ + // add a file & header + $markdown[$benchmark][] = sprintf("# %s\n", $benchmark); + $markdown[$benchmark][] = sprintf("`%s`\n", $json['benchmark'][$benchmark]['class']); + + foreach(array_keys($json['benchmark'][$benchmark]['subjects']) as $subject){ + // list item + $list[$benchmark][] = $subject; + + $subj = $json['benchmark'][$benchmark]['subjects'][$subject]; + + $markdown[$benchmark][] = sprintf("## %s\n", $subject); + $markdown[$benchmark][] = sprintf("**Revs: %s, Iterations: %s**\n", $subj['revs'], $subj['iterations']); + $markdown[$benchmark][] = parseVariants($subj['variants']); + $markdown[$benchmark][] = ''; + } + + $markdown[$benchmark][] = '[back to overview](./Benchmark.md)'; +} + +// create overview list +foreach($list as $benchmark => $subjects){ + // list item + $markdown['Readme'][] = sprintf('- [%1$s](./%1$s.md)', $benchmark); + + foreach($subjects as $subject){ + // list sub-item + $markdown['Readme'][] = sprintf(' - [%2$s](./%1$s.md#%3$s)', $benchmark, $subject, strtolower($subject)); + } +} + + +if(!is_dir(BUILDDIR.'/markdown/')){ + mkdir(BUILDDIR.'/markdown/'); +} + +foreach($markdown as $file => $content){ + file_put_contents(BUILDDIR.'/markdown/'.$file.'.md', implode("\n", $content)); +} diff --git a/benchmark/parse-common.php b/benchmark/parse-common.php new file mode 100644 index 000000000..eacbc2357 --- /dev/null +++ b/benchmark/parse-common.php @@ -0,0 +1,78 @@ + + * @copyright 2024 smiley + * @license MIT + */ +declare(strict_types=1); + +namespace chillerlan\QRCodeBenchmark; + +use function array_column; +use function array_key_first; +use function array_keys; +use function array_map; +use function array_sum; +use function explode; +use function implode; +use function intdiv; +use function rsort; +use function sprintf; +use function str_repeat; +use const SORT_NUMERIC; + +const BUILDDIR = __DIR__.'/../.build/phpbench'; +const FILE = BUILDDIR.'/benchmark'; // without extension + +function parseVariants(array $variants):string{ + $data = []; + + foreach($variants as $variant){ + [$version, $ecc, $mode] = explode(',', $variant['name']); + + $data[(int)$version][$mode][$ecc] = parseVariantResults($variant['results']); + } + + $v = $data[array_key_first($data)]; + $modeKeys = array_keys($v); + $eccKeys = array_keys($v[$modeKeys[0]]); + + $modeHeaders = array_map(fn($mode) => sprintf('%s', $mode), $modeKeys); + $eccHeaders = array_map(fn($ecc) => sprintf('%s', $ecc), $eccKeys); + + $table = ['']; + $table[] = sprintf('%s', implode('', $modeHeaders)); + $table[] = sprintf('%s', str_repeat(implode('', $eccHeaders), count($modeKeys))); + $table[] = ''; + + foreach($data as $version => $modes){ + + $results = []; + + foreach($modes as $eccLevels){ + foreach($eccLevels as [$time_avg, $mem_peak]){ + $results[] = sprintf('', $time_avg); + } + } + + $table[] = sprintf('%s', $version, implode('', $results)); + + } + + $table[] = '
Version
%01.3f
%s
'; + + return implode("\n", $table); +} + +function parseVariantResults(array $results):array{ + $iterations = count($results); + $mem_peak = array_column($results, 'mem_peak'); + + rsort($mem_peak, SORT_NUMERIC); + + return [ + (array_sum(array_column($results, 'time_avg')) / $iterations / 1000), + intdiv($mem_peak[0], 1024), + ]; +} diff --git a/benchmark/parse-result.php b/benchmark/parse-result.php new file mode 100644 index 000000000..931e97031 --- /dev/null +++ b/benchmark/parse-result.php @@ -0,0 +1,131 @@ + + * @copyright 2024 smiley + * @license MIT + */ +declare(strict_types=1); + +namespace chillerlan\QRCodeBenchmark; + +use function array_combine; +use function array_map; +use function array_shift; +use function explode; +use function file_get_contents; +use function file_put_contents; +use function floatval; +use function intval; +use function json_encode; +use function str_replace; +use function str_starts_with; +use function trim; +use const JSON_PRESERVE_ZERO_FRACTION; +use const JSON_PRETTY_PRINT; + +require_once __DIR__.'/parse-common.php'; + +const SEPARATOR = '|'; + +function parseLine(string $line):array{ + return array_map(fn(string $str):string => trim($str, "\ \n\r\t\v\0\""), explode(SEPARATOR, $line)); +} + +$csv = explode("\n", trim(file_get_contents(FILE.'.csv'))); +$header = parseLine(array_shift($csv)); +$parsed = array_map(fn(string $line):array => array_combine($header, parseLine($line)), $csv); +$json = ['env' => [], 'suite' => [], 'benchmark' => []]; + +foreach($parsed as $i => $result){ + + // booleans + foreach(['has_baseline', 'env_php_xdebug', 'env_opcache_extension_loaded', 'env_opcache_enabled'] as $bool){ + $result[$bool] = (bool)$result[$bool]; + } + + // integers + foreach(['variant_index', 'variant_revs', 'variant_iterations', 'iteration_index', 'result_time_net', 'result_time_revs', 'result_mem_peak', 'result_mem_real', 'result_mem_final'] as $int){ + $result[$int] = intval($result[$int]); + } + + // floats + foreach(['env_sampler_nothing', 'env_sampler_md5', 'env_sampler_file_rw', 'result_time_avg', 'result_comp_z_value', 'result_comp_deviation'] as $float){ + $result[$float] = floatval($result[$float]); + } + + // arrays + foreach(['subject_groups', 'variant_params'] as $array){ + $val = trim($result[$array], '"[]'); + + if($val === ''){ + $result[$array] = []; + + continue; + } + + $val = array_map('trim', explode(',', $val)); + + $result[$array] = $val; + } + + // rename some things to avoid bloat + $result['subject_revs'] = $result['variant_revs']; + $result['subject_iterations'] = $result['variant_iterations']; + $result['result_index'] = $result['iteration_index']; + + unset($result['variant_revs'], $result['variant_iterations'], $result['iteration_index'], $result['result_time_revs']); + + // add the class name + $json['benchmark'][$result['benchmark_name']]['class'] = $result['benchmark_class']; + + foreach($result as $k => $v){ + + // the environment info is only needed once + if(str_starts_with($k, 'env_')){ + + if($i === 0){ + $json['env'][str_replace('env_', '', $k)] = $v; + } + + continue; + } + + // suite info is needed only once + if(str_starts_with($k, 'suite_')){ + + if($i === 0){ + $json['suite'][str_replace('suite_', '', $k)] = $v; + } + + continue; + } + + // test subject info once per test subject + if(str_starts_with($k, 'subject_')){ + + // skip the name as it is the key + if($k === 'subject_name'){ + continue; + } + + $json['benchmark'][$result['benchmark_name']]['subjects'][$result['subject_name']][str_replace('subject_', '', $k)] = $v; + } + + // add variants + if(str_starts_with($k, 'variant_')){ + $json['benchmark'][$result['benchmark_name']]['subjects'][$result['subject_name']]['variants'][$result['variant_index']][str_replace('variant_', '', $k)] = $v; + } + + // add benchmark results per variant + if(str_starts_with($k, 'result_')){ + $json['benchmark'][$result['benchmark_name']]['subjects'][$result['subject_name']]['variants'][$result['variant_index']]['results'][$result['result_index']][str_replace('result_', '', $k)] = $v; + } + + } + +} + +file_put_contents(FILE.'.json', json_encode($json, (JSON_PRESERVE_ZERO_FRACTION|JSON_PRETTY_PRINT))); diff --git a/composer.json b/composer.json index acb10e812..e1191710e 100644 --- a/composer.json +++ b/composer.json @@ -52,6 +52,7 @@ }, "require-dev": { "chillerlan/php-authenticator": "^5.1", + "phpbench/phpbench": "^1.2.15", "phan/phan": "^5.4", "phpunit/phpunit": "^11.0", "phpmd/phpmd": "^2.15", @@ -70,10 +71,15 @@ }, "autoload-dev": { "psr-4": { + "chillerlan\\QRCodeBenchmark\\": "benchmark/", "chillerlan\\QRCodeTest\\": "tests/" } }, "scripts": { + "phpbench":[ + "Composer\\Config::disableProcessTimeout", + "@php vendor/bin/phpbench run" + ], "phpunit": "@php vendor/bin/phpunit", "phan": "@php vendor/bin/phan" }, diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 000000000..dcd98e74b --- /dev/null +++ b/phpbench.json @@ -0,0 +1,19 @@ +{ + "$schema": "vendor/phpbench/phpbench/phpbench.schema.json", + "core.extensions": [ + "PhpBench\\Extensions\\XDebug\\XDebugExtension" + ], + "xdebug.command_handler_output_dir": ".build/phpbench/xdebug-profile", + "runner.bootstrap": "vendor/autoload.php", + "runner.path": "benchmark", + "runner.file_pattern": "*Benchmark.php", + "console.ansi": true, + "report.outputs": { + "csv": { + "extends": "delimited", + "delimiter": "|", + "file": ".build/phpbench/benchmark.csv", + "header": true + } + } +} diff --git a/tests/Performance/PerformanceTest.php b/tests/Performance/PerformanceTest.php deleted file mode 100644 index 9fc9137f0..000000000 --- a/tests/Performance/PerformanceTest.php +++ /dev/null @@ -1,48 +0,0 @@ - - * @copyright 2023 smiley - * @license MIT - */ - -namespace chillerlan\QRCodeTest\Performance; - -use Closure; -use function hrtime; - -/** - * - */ -class PerformanceTest{ - - protected int $runs; - protected int $total = 0; - - public function __construct(int $runs = 1000){ - $this->runs = $runs; - } - - public function run(Closure $subject):self{ - $this->total = 0; - - for($i = 0; $i < $this->runs; $i++){ - $start = hrtime(true); - - $subject(); - - $end = hrtime(true); - - $this->total += ($end - $start); - } - - return $this; - } - - public function getResult():float{ - return ($this->total / $this->runs / 1000000); - } - -} diff --git a/tests/Performance/maskpattern.php b/tests/Performance/maskpattern.php deleted file mode 100644 index 55295b79a..000000000 --- a/tests/Performance/maskpattern.php +++ /dev/null @@ -1,62 +0,0 @@ - - * @copyright 2023 smiley - * @license MIT - */ - -namespace chillerlan\QRCodeTest\Performance; - -use chillerlan\QRCode\{QRCode, QROptions}; -use chillerlan\QRCode\Common\{EccLevel, MaskPattern, Mode, Version}; -use chillerlan\QRCodeTest\QRMaxLengthTrait; -use Generator; -use function file_put_contents; -use function json_encode; -use function printf; -use function sprintf; -use function str_repeat; -use function substr; -use const JSON_PRETTY_PRINT; - -require_once __DIR__.'/../../vendor/autoload.php'; - -// excerpt from QRCodeReaderTestAbstract -$generator = new class () { - use QRMaxLengthTrait; - - public function dataProvider():Generator{ - $str = str_repeat('https://www.youtube.com/watch?v=dQw4w9WgXcQ ', 100); - $eccLevel = new EccLevel(EccLevel::H); - - for($v = 1; $v <= 40; $v++){ - $version = new Version($v); - - yield sprintf('version %2s%s', $version, $eccLevel) => [ - $version->getVersionNumber(), - $eccLevel->getLevel(), - substr($str, 0, self::getMaxLengthForMode(Mode::BYTE, $version, $eccLevel)), - ]; - } - } - -}; - -$test = new PerformanceTest(100); -$json = []; - -foreach($generator->dataProvider() as $key => [$version, $eccLevel, $data]){ - $qrcode = new QRCode(new QROptions(['version' => $version, 'eccLevel' => $eccLevel])); - $qrcode->addByteSegment($data); - $matrix = $qrcode->getQRMatrix(); - - $test->run(fn() => MaskPattern::getBestPattern($matrix)); - - printf("%s: %01.3fms\n", $key, $test->getResult()); - $json[$version] = $test->getResult(); -} - -file_put_contents(__DIR__.'/performance_maskpattern.json', json_encode($json, JSON_PRETTY_PRINT)); diff --git a/tests/Performance/output.php b/tests/Performance/output.php deleted file mode 100644 index 98a0d9fe8..000000000 --- a/tests/Performance/output.php +++ /dev/null @@ -1,80 +0,0 @@ - - * @copyright 2023 smiley - * @license MIT - */ - -namespace chillerlan\QRCodeTest\Performance; - -use chillerlan\QRCode\{QRCode, QROptions}; -use chillerlan\QRCode\Common\{EccLevel, Mode, Version}; -use chillerlan\QRCode\Output\QROutputInterface; -use chillerlan\QRCodeTest\QRMaxLengthTrait; -use Generator; -use function file_put_contents; -use function json_encode; -use function printf; -use function sprintf; -use function str_repeat; -use function str_replace; -use function substr; -use const JSON_PRETTY_PRINT; - -require_once __DIR__.'/../../vendor/autoload.php'; - - -// excerpt from QRCodeReaderTestAbstract -$generator = new class () { - use QRMaxLengthTrait; - - public function dataProvider():Generator{ - $str = str_repeat('https://www.youtube.com/watch?v=dQw4w9WgXcQ ', 100); - $eccLevel = new EccLevel(EccLevel::L); - - for($v = 5; $v <= 40; $v += 5){ - $version = new Version($v); - foreach(QROutputInterface::MODES as $FQN){ - $name = str_replace('chillerlan\\QRCode\\Output\\', '', $FQN); - - yield sprintf('version %2s: %-14s', $version, $name) => [ - $version->getVersionNumber(), - $FQN, - substr($str, 0, self::getMaxLengthForMode(Mode::BYTE, $version, $eccLevel)), - $name, - ]; - } - } - - } - -}; - -$test = new PerformanceTest(100); -$json = []; - -foreach($generator->dataProvider() as $key => [$version, $FQN, $data, $name]){ - - $options = new QROptions([ - 'version' => $version, - 'outputInterface' => $FQN, - 'connectPaths' => true, - 'drawLightModules' => true, - 'drawCircularModules' => true, - 'gdImageUseUpscale' => false, // set to false to allow proper comparison - ]); - - $qrcode = new QRCode($options); - $qrcode->addByteSegment($data); - $matrix = $qrcode->getQRMatrix(); - - $test->run(fn() => (new $FQN($options, $matrix))->dump()); - - printf("%s: %8.3fms\n", $key, $test->getResult()); - $json[$name][$version] = $test->getResult(); -} - -file_put_contents(__DIR__.'/performance_output.json', json_encode($json, JSON_PRETTY_PRINT)); diff --git a/tests/Performance/qrcode.php b/tests/Performance/qrcode.php deleted file mode 100644 index e08e76121..000000000 --- a/tests/Performance/qrcode.php +++ /dev/null @@ -1,84 +0,0 @@ - - * @copyright 2023 smiley - * @license MIT - */ - -namespace chillerlan\QRCodeTest\Performance; - -use chillerlan\QRCode\{QRCode, QROptions}; -use chillerlan\QRCode\Common\{EccLevel, Mode, Version}; -use chillerlan\QRCode\Output\QRStringJSON; -use chillerlan\QRCodeTest\QRMaxLengthTrait; -use Generator; -use function file_put_contents; -use function json_encode; -use function mb_substr; -use function printf; -use function sprintf; -use function str_repeat; -use function str_replace; -use const JSON_PRETTY_PRINT; - -require_once __DIR__.'/../../vendor/autoload.php'; - -// excerpt from QRCodeReaderTestAbstract -$generator = new class () { - use QRMaxLengthTrait; - - public function dataProvider():Generator{ - - $dataModeData = [ - Mode::NUMBER => str_repeat('0123456789', 750), - Mode::ALPHANUM => str_repeat('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 $%*+-./:', 100), - Mode::KANJI => str_repeat('漂う花の香り', 350), - Mode::HANZI => str_repeat('无可奈何燃花作香', 250), - Mode::BYTE => str_repeat('https://www.youtube.com/watch?v=dQw4w9WgXcQ ', 100), - ]; - - foreach(Mode::INTERFACES as $dataMode => $dataModeInterface){ - $dataModeName = str_replace('chillerlan\\QRCode\\Data\\', '', $dataModeInterface); - - for($v = 1; $v <= 40; $v++){ - $version = new Version($v); - - foreach([EccLevel::L, EccLevel::M, EccLevel::Q, EccLevel::H] as $ecc){ - $eccLevel = new EccLevel($ecc); - - yield sprintf('version %2s%s (%s)', $version, $eccLevel, $dataModeName) => [ - $version->getVersionNumber(), - $eccLevel, - $dataModeInterface, - $dataModeName, - mb_substr($dataModeData[$dataMode], 0, self::getMaxLengthForMode($dataMode, $version, $eccLevel)), - ]; - } - } - } - - } - -}; - -$test = new PerformanceTest(100); -$json = []; - -foreach($generator->dataProvider() as $key => [$version, $eccLevel, $dataModeInterface, $dataModeName, $data]){ - - $options = new QROptions([ - 'outputInterface' => QRStringJSON::class, - 'version' => $version, - 'eccLevel' => $eccLevel->getLevel(), - ]); - - $test->run(fn() => (new QRCode($options))->addSegment(new $dataModeInterface($data))->render()); - - printf("%s: %01.3fms\n", $key, $test->getResult()); - $json[$dataModeName][(string)$eccLevel][$version] = $test->getResult(); -} - -file_put_contents(__DIR__.'/performance_qrcode.json', json_encode($json, JSON_PRETTY_PRINT)); diff --git a/tests/Performance/qrdata.php b/tests/Performance/qrdata.php deleted file mode 100644 index 615baa8ca..000000000 --- a/tests/Performance/qrdata.php +++ /dev/null @@ -1,92 +0,0 @@ - - * @copyright 2023 smiley - * @license MIT - */ - -namespace chillerlan\QRCodeTest\Performance; - -use chillerlan\QRCode\Common\{EccLevel, Mode, Version}; -use chillerlan\QRCode\Data\QRData; -use chillerlan\QRCode\QROptions; -use chillerlan\QRCodeTest\QRMaxLengthTrait; -use Generator; -use function file_put_contents; -use function json_encode; -use function printf; -use function sprintf; -use function str_repeat; -use function str_replace; -use const JSON_PRETTY_PRINT; - -require_once __DIR__.'/../../vendor/autoload.php'; - -// excerpt from QRCodeReaderTestAbstract -$generator = new class () { - use QRMaxLengthTrait; - - public function dataProvider():Generator{ - - $dataModeData = [ - Mode::NUMBER => str_repeat('0123456789', 750), - Mode::ALPHANUM => str_repeat('ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890 $%*+-./:', 100), - Mode::KANJI => str_repeat('漂う花の香り', 350), - Mode::HANZI => str_repeat('无可奈何燃花作香', 250), - Mode::BYTE => str_repeat('https://www.youtube.com/watch?v=dQw4w9WgXcQ ', 100), - ]; - - foreach(Mode::INTERFACES as $dataMode => $dataModeInterface){ - $dataModeName = str_replace('chillerlan\\QRCode\\Data\\', '', $dataModeInterface); - - for($v = 1; $v <= 40; $v++){ - $version = new Version($v); - - foreach([EccLevel::L, EccLevel::M, EccLevel::Q, EccLevel::H] as $ecc){ - $eccLevel = new EccLevel($ecc); - - yield sprintf('version %2s%s (%s)', $version, $eccLevel, $dataModeName) => [ - $version->getVersionNumber(), - $eccLevel, - $dataModeInterface, - $dataModeName, - mb_substr($dataModeData[$dataMode], 0, self::getMaxLengthForMode($dataMode, $version, $eccLevel)), - ]; - } - } - } - - } - -}; - -$test = new PerformanceTest(100); -$json = []; - -foreach($generator->dataProvider() as $key => [$version, $eccLevel, $dataModeInterface, $dataModeName, $data]){ - // invovcation tests the performance of QRData::writeBitBuffer() - $test->run(fn() => new QRData(new QROptions(['version' => $version, 'eccLevel' => $eccLevel->getLevel()]), [new $dataModeInterface($data)])); - - printf('%s encode: % 6.3fms', $key, $test->getResult()); - $json[$dataModeName][(string)$eccLevel]['encode'][$version] = $test->getResult(); - - // writeMatrix includes QRMatrix::writeCodewords() and the ReedSolomonEncoder - $qrdata = new QRData(new QROptions(['version' => $version, 'eccLevel' => $eccLevel->getLevel()]), [new $dataModeInterface($data)]); - $test->run(fn() => $qrdata->writeMatrix()); - - printf(', write matrix: % 6.3fms', $test->getResult()); - $json[$dataModeName][(string)$eccLevel]['write'][$version] = $test->getResult(); - - $bitBuffer = $qrdata->getBitBuffer(); - $bitBuffer->read(4); // read data mode indicator - - $test->run(fn() => $dataModeInterface::decodeSegment(clone $bitBuffer, $version)); - - printf(", decode: % 6.3fms\n", $test->getResult()); - $json[$dataModeName][(string)$eccLevel]['decode'][$version] = $test->getResult(); -} - -file_put_contents(__DIR__.'/performance_qrdata.json', json_encode($json, JSON_PRETTY_PRINT));